@amaster.ai/dsh-langfuse 0.1.23 → 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 +14 -6
- package/lib/hooks.js +6 -1
- package/lib/index.js +94 -27
- package/lib/types/hooks.d.ts +8 -0
- package/lib/types/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Langfuse observability for [DeepSeek Harness (dsh)](https://github.com/deepseek-ai/deepseek-harness):
|
|
6
6
|
|
|
7
7
|
- one **generation** per LLM call (`llm/stream` waterfall), plus a nested `llm-request` span carrying the verbatim loop-built request
|
|
8
|
-
- one **tool observation** per tool call (`tools/execute`
|
|
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
10
|
- one **trace** per session turn (`session/event`) — in the v5 SDK the trace IS its root span, ended (and thereby exported) at `turn/end`
|
|
11
11
|
- subagent child sessions nested under the parent's tree (`session/created` header link + `subagent/start` / `subagent/end`)
|
|
@@ -87,7 +87,7 @@ Generation `usageDetails` has disjoint `input`, `cache_read_input_tokens`, `cach
|
|
|
87
87
|
|
|
88
88
|
Compaction and session-title calls passing through `llm/stream` already receive their own generation and `purpose` metadata, including out-of-turn calls. Tools using the same LLM service are observed there too; adding a second generation from tool-result usage would duplicate accounting.
|
|
89
89
|
|
|
90
|
-
Hook spans appear when a dsh hook bridge emits the paired session records.
|
|
90
|
+
Hook spans appear when a dsh hook bridge emits the paired session records. With content capture enabled, they show `hook:<point>:<name>`, optional input and parsed result, and available bounded process output alongside execution status. Producers must emit these fields; upgrading this plugin cannot recover omitted data. `SessionStart` only appears if a producer reports it and the installed runtime admits the records.
|
|
91
91
|
|
|
92
92
|
### Custom hook producers
|
|
93
93
|
|
|
@@ -115,6 +115,7 @@ appendHookEvent(session, 'hook/invoked', {
|
|
|
115
115
|
...identity,
|
|
116
116
|
dialect: 'company-validator',
|
|
117
117
|
name: 'Validate request',
|
|
118
|
+
input: { request: 'example' },
|
|
118
119
|
});
|
|
119
120
|
// After execution, including exceptions, timeout or cancellation:
|
|
120
121
|
appendHookEvent(session, 'hook/result', {
|
|
@@ -123,22 +124,29 @@ appendHookEvent(session, 'hook/result', {
|
|
|
123
124
|
outcome: 'success',
|
|
124
125
|
exitCode: 0,
|
|
125
126
|
durationMs: 37,
|
|
127
|
+
result: { reason: 'valid', additionalContext: 'checked', continue: true },
|
|
126
128
|
});
|
|
127
129
|
```
|
|
128
130
|
|
|
129
|
-
Every custom execution should have a unique `invocationId` and a result emitted in the producer's `finally` path. Both records must share `turn`, `point`, `handlerId`, and `invocationId`. `handlerId` identifies the configured handler; `invocationId` identifies one execution, including concurrent executions of that handler. Optional `name` labels the
|
|
131
|
+
Every custom execution should have a unique `invocationId` and a result emitted in the producer's `finally` path. Both records must share `turn`, `point`, `handlerId`, and `invocationId`. `handlerId` identifies the configured handler; `invocationId` identifies one execution, including concurrent executions of that handler. Optional `name` labels the observation when content capture is enabled. `HookInvoked.input` carries the script payload; `HookResult.result` carries the parsed output object, including available reasons, additional context and continue/stop instructions. The consumer preserves these producer-defined fields without interpreting them as decisions. Unmatched/skipped hooks must not emit an invocation: these records represent actual execution. Optional `stdout`, `stderr` and legacy `stderrSummary` are masked, then limited to 4096 characters each in observations, with `stdoutTruncated`, `stderrTruncated` and `stderrSummaryTruncated` metadata. Producers should also bound persisted process output and omit unavailable optional fields rather than writing `undefined` into the JSON session log.
|
|
132
|
+
|
|
133
|
+
Tool hooks should include the execution's `callId` in both records. The plugin matches it within the originating session and turn; it never chooses the last active tool. Tool observations begin at `tools/pre-execute` (a prepended observer) and end at `tools/result`, covering synchronous/awaited BeforeTool and AfterTool hooks, pre-dispatch denials, and final post-policy output. Bridges must run BeforeTool within the pre-execute waterfall and AfterTool within post-execute, before the final result notification. A bridge registered with prepend ahead of this observer must delegate before reporting its hook. Agent hooks and legacy records without `callId` remain under the turn/subagent. Unknown or already finished calls fall back to that parent; late invocations outside the originating turn use the detached-root behavior below.
|
|
130
134
|
|
|
131
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.
|
|
132
136
|
|
|
133
|
-
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.
|
|
134
138
|
|
|
135
|
-
Custom names and
|
|
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).
|
|
136
140
|
|
|
137
141
|
Observability is a no-throw seam: backend errors are logged with a `[dsh-langfuse]` prefix and never escape into the agent loop.
|
|
138
142
|
|
|
139
143
|
## Compatibility
|
|
140
144
|
|
|
141
|
-
Run `pnpm test:sdk` at the repository root for a secrets-free check using the real Langfuse SDK and the existing local OTLP receiver. It verifies export, native tool type, SDK label redaction, ambient-baggage isolation, external trace ownership, agent-less tool unload, and custom hook export across turn closure. It is separate from the mocked unit suite and the gateway/real-Langfuse E2E legs.
|
|
145
|
+
Run `pnpm test:sdk` at the repository root for a secrets-free check using the real Langfuse SDK and the existing local OTLP receiver. It verifies export, native tool type, SDK label redaction, ambient-baggage isolation, external trace ownership, agent-less tool unload, the real tool runtime and hook processes, tool-hook parenting and enclosing timestamps, content masking, and custom hook export across turn closure. It is separate from the mocked unit suite and the gateway/real-Langfuse E2E legs.
|
|
146
|
+
|
|
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
|
+
|
|
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`.
|
|
142
150
|
|
|
143
151
|
### Trace overview compatibility
|
|
144
152
|
|
package/lib/hooks.js
CHANGED
|
@@ -8,6 +8,7 @@ function baseFields(data) {
|
|
|
8
8
|
value.point.length > 0 &&
|
|
9
9
|
typeof value.handlerId === 'string' &&
|
|
10
10
|
value.handlerId.length > 0 &&
|
|
11
|
+
(value.callId === undefined || (typeof value.callId === 'string' && value.callId.length > 0)) &&
|
|
11
12
|
(value.invocationId === undefined ||
|
|
12
13
|
(typeof value.invocationId === 'string' && value.invocationId.length > 0)));
|
|
13
14
|
}
|
|
@@ -27,7 +28,11 @@ export function isHookResult(data) {
|
|
|
27
28
|
['success', 'failed', 'timed-out', 'cancelled'].includes(data.outcome)) &&
|
|
28
29
|
(data.exitCode === undefined || Number.isSafeInteger(data.exitCode)) &&
|
|
29
30
|
(data.signal === undefined || typeof data.signal === 'string') &&
|
|
30
|
-
(data.stderrSummary === undefined || typeof data.stderrSummary === 'string')
|
|
31
|
+
(data.stderrSummary === undefined || typeof data.stderrSummary === 'string') &&
|
|
32
|
+
(data.stdout === undefined || typeof data.stdout === 'string') &&
|
|
33
|
+
(data.stderr === undefined || typeof data.stderr === 'string') &&
|
|
34
|
+
(data.result === undefined ||
|
|
35
|
+
(data.result !== null && typeof data.result === 'object' && !Array.isArray(data.result))));
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
38
|
* Append standard telemetry without changing hook decisions.
|
package/lib/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* from the chunk stream), plus a nested `llm-request` **span** holding the
|
|
9
9
|
* same options and collected chunk stream (the rawest request/response a
|
|
10
10
|
* plugin can observe — the provider HTTP body is assembled inside the adapter).
|
|
11
|
-
* - `tools/execute`
|
|
11
|
+
* - `tools/pre-execute` → `tools/result` → one Langfuse **span** per tool pipeline
|
|
12
12
|
* (routed to the session's trace via `exec.agent.id` — the agent/session
|
|
13
13
|
* shared identity).
|
|
14
14
|
* - `hook/invoked` / `hook/result` session records → one **span** per hook run.
|
|
@@ -204,8 +204,21 @@ 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
|
-
|
|
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
|
+
}
|
|
218
|
+
/** Per-session stack of currently open tool pipeline spans (for subagent parenting). */
|
|
208
219
|
const openToolSpans = new Map();
|
|
220
|
+
const toolCalls = new Map();
|
|
221
|
+
const toolResults = new WeakMap();
|
|
209
222
|
const openHooks = new Map();
|
|
210
223
|
const openOperations = new Set();
|
|
211
224
|
const traceContexts = new Map();
|
|
@@ -395,7 +408,10 @@ export function apply(ctx, config) {
|
|
|
395
408
|
reporter.endSpan(previous.root, { level: 'WARNING' });
|
|
396
409
|
const child = children.get(sessionId);
|
|
397
410
|
const inTurn = child ? child.turn === data.turn : sessions.get(sessionId)?.turn === data.turn;
|
|
398
|
-
const
|
|
411
|
+
const completed = data.invocationId && !inTurn
|
|
412
|
+
? completedTurns.get(JSON.stringify([sessionId, data.turn]))
|
|
413
|
+
: undefined;
|
|
414
|
+
const root = data.invocationId && !inTurn && !completed
|
|
399
415
|
? // A late hook must not consume context queued for the next turn.
|
|
400
416
|
reporter.openTrace({
|
|
401
417
|
name: config.traceName,
|
|
@@ -403,8 +419,16 @@ export function apply(ctx, config) {
|
|
|
403
419
|
metadata: { turn: data.turn, detachedHook: true },
|
|
404
420
|
})
|
|
405
421
|
: undefined;
|
|
406
|
-
const
|
|
407
|
-
|
|
422
|
+
const tool = data.callId ? toolCalls.get(sessionId)?.get(data.callId) : undefined;
|
|
423
|
+
const hookName = config.captureContent ? reporter.mask({ name: data.name }).name : undefined;
|
|
424
|
+
const span = reporter.startSpan(completed?.parent ??
|
|
425
|
+
(root === undefined
|
|
426
|
+
? tool?.turn === data.turn
|
|
427
|
+
? tool.span
|
|
428
|
+
: parentFor(sessionId)
|
|
429
|
+
: root), {
|
|
430
|
+
name: `hook:${data.point}${hookName ? `:${hookName}` : ''}`,
|
|
431
|
+
input: config.captureContent ? data.input : undefined,
|
|
408
432
|
metadata: {
|
|
409
433
|
turn: data.turn,
|
|
410
434
|
point: data.point,
|
|
@@ -412,6 +436,7 @@ export function apply(ctx, config) {
|
|
|
412
436
|
matcher: data.matcher,
|
|
413
437
|
handlerId: data.handlerId,
|
|
414
438
|
...(data.invocationId ? { invocationId: data.invocationId } : {}),
|
|
439
|
+
...(data.callId ? { callId: data.callId } : {}),
|
|
415
440
|
...(config.captureContent && data.name ? { name: data.name } : {}),
|
|
416
441
|
},
|
|
417
442
|
});
|
|
@@ -422,7 +447,12 @@ export function apply(ctx, config) {
|
|
|
422
447
|
point: data.point,
|
|
423
448
|
handlerId: data.handlerId,
|
|
424
449
|
persistent: !!data.invocationId,
|
|
425
|
-
|
|
450
|
+
callId: data.callId,
|
|
451
|
+
turnTrace: completed
|
|
452
|
+
? completed.turnTrace
|
|
453
|
+
: root === undefined
|
|
454
|
+
? (child?.turnTrace ?? sessions.get(sessionId)?.trace)
|
|
455
|
+
: undefined,
|
|
426
456
|
});
|
|
427
457
|
openHooks.set(sessionId, pending);
|
|
428
458
|
return;
|
|
@@ -437,7 +467,8 @@ export function apply(ctx, config) {
|
|
|
437
467
|
if (!hook ||
|
|
438
468
|
hook.handlerId !== data.handlerId ||
|
|
439
469
|
hook.turn !== data.turn ||
|
|
440
|
-
hook.point !== data.point
|
|
470
|
+
hook.point !== data.point ||
|
|
471
|
+
(data.callId !== undefined && hook.callId !== data.callId))
|
|
441
472
|
return;
|
|
442
473
|
const span = hook.span;
|
|
443
474
|
const failed = data.outcome
|
|
@@ -449,6 +480,21 @@ export function apply(ctx, config) {
|
|
|
449
480
|
? 'WARNING'
|
|
450
481
|
: 'DEFAULT';
|
|
451
482
|
if (span) {
|
|
483
|
+
const processOutput = {};
|
|
484
|
+
const truncation = {};
|
|
485
|
+
if (config.captureContent) {
|
|
486
|
+
const masked = reporter.mask({
|
|
487
|
+
stdout: data.stdout,
|
|
488
|
+
stderr: data.stderr,
|
|
489
|
+
stderrSummary: data.stderrSummary,
|
|
490
|
+
});
|
|
491
|
+
for (const [field, text] of Object.entries(masked)) {
|
|
492
|
+
if (text === undefined)
|
|
493
|
+
continue;
|
|
494
|
+
processOutput[field] = text.slice(0, 4096);
|
|
495
|
+
truncation[`${field}Truncated`] = text.length > 4096;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
452
498
|
if (failed && sessions.get(rootSessionId(sessionId) ?? '')?.trace === hook.turnTrace)
|
|
453
499
|
noteError(sessionId);
|
|
454
500
|
reporter.endSpan(span, {
|
|
@@ -457,10 +503,10 @@ export function apply(ctx, config) {
|
|
|
457
503
|
...(data.outcome ? { outcome: data.outcome } : {}),
|
|
458
504
|
...(data.signal ? { signal: data.signal } : {}),
|
|
459
505
|
exitCode: data.exitCode,
|
|
460
|
-
|
|
506
|
+
...(config.captureContent ? { result: data.result, ...processOutput } : {}),
|
|
461
507
|
},
|
|
462
508
|
level,
|
|
463
|
-
metadata: { durationMs: data.durationMs },
|
|
509
|
+
metadata: { durationMs: data.durationMs, ...truncation },
|
|
464
510
|
});
|
|
465
511
|
}
|
|
466
512
|
reporter.endSpan(hook.root ?? null, { level });
|
|
@@ -469,14 +515,18 @@ export function apply(ctx, config) {
|
|
|
469
515
|
openHooks.delete(sessionId);
|
|
470
516
|
return;
|
|
471
517
|
}
|
|
518
|
+
if (event.type === 'turn/end' || event.type === 'turn/start')
|
|
519
|
+
toolCalls.delete(sessionId);
|
|
472
520
|
if (event.type === 'turn/end')
|
|
473
521
|
closeHooks(sessionId, true);
|
|
474
522
|
const child = children.get(sessionId);
|
|
475
523
|
if (child) {
|
|
476
524
|
if (event.type === 'turn/start')
|
|
477
525
|
child.turn = event.data.turn;
|
|
478
|
-
if (event.type === 'turn/end')
|
|
526
|
+
if (event.type === 'turn/end') {
|
|
527
|
+
rememberTurn(sessionId, event.data.turn, child.span, child.turnTrace ?? null);
|
|
479
528
|
child.turn = undefined;
|
|
529
|
+
}
|
|
480
530
|
// The delegated prompt (the child's first user/message) is the
|
|
481
531
|
// subagent span's input; the descriptor enriches the span name.
|
|
482
532
|
if (event.type === 'user/message' && !child.hasInput && config.captureContent) {
|
|
@@ -528,6 +578,7 @@ export function apply(ctx, config) {
|
|
|
528
578
|
const state = sessions.get(sessionId);
|
|
529
579
|
if (state) {
|
|
530
580
|
sessions.delete(sessionId);
|
|
581
|
+
rememberTurn(sessionId, event.data.turn, state.trace, state.trace);
|
|
531
582
|
// The turn's final answer becomes the trace's output; ending the
|
|
532
583
|
// root span is what exports the trace (v5 exports spans on end).
|
|
533
584
|
afterMedia(state.trace, (endedAt) => reporter.endSpan(state.trace, {
|
|
@@ -560,7 +611,12 @@ export function apply(ctx, config) {
|
|
|
560
611
|
const state = sessions.get(sessionId);
|
|
561
612
|
sessions.delete(sessionId);
|
|
562
613
|
traceContexts.delete(sessionId);
|
|
614
|
+
for (const [key, completed] of completedTurns) {
|
|
615
|
+
if (completed.sessionId === sessionId)
|
|
616
|
+
completedTurns.delete(key);
|
|
617
|
+
}
|
|
563
618
|
openToolSpans.delete(sessionId);
|
|
619
|
+
toolCalls.delete(sessionId);
|
|
564
620
|
// End an abandoned root (crash-orphaned turn or between-turns one-off
|
|
565
621
|
// trace) — v5 spans only export on end, so dropping it open would lose
|
|
566
622
|
// everything it recorded.
|
|
@@ -756,12 +812,12 @@ export function apply(ctx, config) {
|
|
|
756
812
|
})();
|
|
757
813
|
});
|
|
758
814
|
// ------------------------------------------------------------------
|
|
759
|
-
// Span per tool
|
|
760
|
-
//
|
|
815
|
+
// Span per tool pipeline, including pre/post hooks and denied calls.
|
|
816
|
+
// Open spans are tracked per session
|
|
761
817
|
// so a subagent span opened mid-dispatch (foreground delegation) parents
|
|
762
818
|
// under the enclosing tool span.
|
|
763
819
|
// ------------------------------------------------------------------
|
|
764
|
-
ctx.on('tools/execute',
|
|
820
|
+
ctx.on('tools/pre-execute', (exec, next) => {
|
|
765
821
|
const sessionId = exec.agent?.id;
|
|
766
822
|
const parent = parentFor(sessionId);
|
|
767
823
|
// An agent-less dispatch's trace root has no session lifecycle to close
|
|
@@ -778,6 +834,12 @@ export function apply(ctx, config) {
|
|
|
778
834
|
});
|
|
779
835
|
if (sessionId && span) {
|
|
780
836
|
openToolSpans.set(sessionId, [...(openToolSpans.get(sessionId) ?? []), span]);
|
|
837
|
+
const calls = toolCalls.get(sessionId) ?? new Map();
|
|
838
|
+
calls.set(exec.callId, {
|
|
839
|
+
span,
|
|
840
|
+
turn: children.get(sessionId)?.turn ?? sessions.get(sessionId)?.turn,
|
|
841
|
+
});
|
|
842
|
+
toolCalls.set(sessionId, calls);
|
|
781
843
|
}
|
|
782
844
|
let closed = false;
|
|
783
845
|
const closeTool = () => finishTool({ level: 'WARNING', statusMessage: 'plugin unloaded during tool call' });
|
|
@@ -787,6 +849,13 @@ export function apply(ctx, config) {
|
|
|
787
849
|
closed = true;
|
|
788
850
|
const endedAt = new Date();
|
|
789
851
|
openOperations.delete(closeTool);
|
|
852
|
+
toolResults.delete(exec);
|
|
853
|
+
if (sessionId && toolCalls.get(sessionId)?.get(exec.callId)?.span === span) {
|
|
854
|
+
const calls = toolCalls.get(sessionId);
|
|
855
|
+
calls.delete(exec.callId);
|
|
856
|
+
if (!calls.size)
|
|
857
|
+
toolCalls.delete(sessionId);
|
|
858
|
+
}
|
|
790
859
|
if (sessionId && span) {
|
|
791
860
|
const remaining = (openToolSpans.get(sessionId) ?? []).filter((s) => s !== span);
|
|
792
861
|
if (remaining.length)
|
|
@@ -807,10 +876,9 @@ export function apply(ctx, config) {
|
|
|
807
876
|
end();
|
|
808
877
|
}
|
|
809
878
|
openOperations.add(closeTool);
|
|
810
|
-
|
|
811
|
-
const result = await next();
|
|
879
|
+
toolResults.set(exec, (result) => {
|
|
812
880
|
if (closed)
|
|
813
|
-
return
|
|
881
|
+
return;
|
|
814
882
|
if (result.isError)
|
|
815
883
|
noteError(sessionId);
|
|
816
884
|
const update = {
|
|
@@ -819,21 +887,18 @@ export function apply(ctx, config) {
|
|
|
819
887
|
statusMessage: result.isError ? contentMessage(config, result.error.message) : undefined,
|
|
820
888
|
metadata: { errorCode: result.isError ? result.error.info?.code : undefined },
|
|
821
889
|
};
|
|
822
|
-
if (span &&
|
|
890
|
+
if (span &&
|
|
891
|
+
config.captureContent &&
|
|
892
|
+
result.content.some((block) => block.type === 'image')) {
|
|
823
893
|
finishTool(update, displayBlocks(result.content, ctx.get('attachments'), mediaOptions, exec.signal));
|
|
824
894
|
}
|
|
825
895
|
else
|
|
826
896
|
finishTool(update);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
level: 'ERROR',
|
|
833
|
-
statusMessage: contentMessage(config, messageOf(error)),
|
|
834
|
-
});
|
|
835
|
-
throw error;
|
|
836
|
-
}
|
|
897
|
+
});
|
|
898
|
+
return next();
|
|
899
|
+
}, { prepend: true });
|
|
900
|
+
ctx.on('tools/result', (exec, result) => {
|
|
901
|
+
toolResults.get(exec)?.(result);
|
|
837
902
|
});
|
|
838
903
|
// Durability checkpoint: the store awaits every session/flush listener.
|
|
839
904
|
ctx.on('session/flush', async () => {
|
|
@@ -859,9 +924,11 @@ export function apply(ctx, config) {
|
|
|
859
924
|
for (const child of children.values())
|
|
860
925
|
reporter.endSpan(child.span, {}, endedAt);
|
|
861
926
|
sessions.clear();
|
|
927
|
+
completedTurns.clear();
|
|
862
928
|
traceContexts.clear();
|
|
863
929
|
children.clear();
|
|
864
930
|
openToolSpans.clear();
|
|
931
|
+
toolCalls.clear();
|
|
865
932
|
await reporter.shutdown();
|
|
866
933
|
};
|
|
867
934
|
});
|
package/lib/types/hooks.d.ts
CHANGED
|
@@ -9,12 +9,20 @@ export interface HookInvoked {
|
|
|
9
9
|
/** Unique per execution, shared with its result; required for concurrent runs. */
|
|
10
10
|
invocationId?: string;
|
|
11
11
|
name?: string;
|
|
12
|
+
/** Tool execution identity; omit for agent hooks. */
|
|
13
|
+
callId?: string;
|
|
14
|
+
input?: unknown;
|
|
12
15
|
}
|
|
13
16
|
export interface HookResult {
|
|
14
17
|
turn: number;
|
|
15
18
|
point: string;
|
|
16
19
|
handlerId: string;
|
|
17
20
|
invocationId?: string;
|
|
21
|
+
callId?: string;
|
|
22
|
+
/** Parsed hook output; process status remains in the fields below. */
|
|
23
|
+
result?: Record<string, unknown>;
|
|
24
|
+
stdout?: string;
|
|
25
|
+
stderr?: string;
|
|
18
26
|
decision: string;
|
|
19
27
|
outcome?: 'success' | 'failed' | 'timed-out' | 'cancelled';
|
|
20
28
|
exitCode?: number;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* from the chunk stream), plus a nested `llm-request` **span** holding the
|
|
9
9
|
* same options and collected chunk stream (the rawest request/response a
|
|
10
10
|
* plugin can observe — the provider HTTP body is assembled inside the adapter).
|
|
11
|
-
* - `tools/execute`
|
|
11
|
+
* - `tools/pre-execute` → `tools/result` → one Langfuse **span** per tool pipeline
|
|
12
12
|
* (routed to the session's trace via `exec.agent.id` — the agent/session
|
|
13
13
|
* shared identity).
|
|
14
14
|
* - `hook/invoked` / `hook/result` session records → one **span** per hook run.
|
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",
|