@amaster.ai/dsh-langfuse 0.1.22 → 0.1.24
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 +11 -5
- package/lib/hooks.js +6 -1
- package/lib/index.js +60 -24
- 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,27 @@ 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
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. Invocations arriving outside their originating turn receive a separate root with `detachedHook: true` and the original turn number; their result ends both hook and root so they export without waiting for another turn. These detached hook roots do not consume `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.
|
|
142
148
|
|
|
143
149
|
### Trace overview compatibility
|
|
144
150
|
|
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,10 @@ 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
|
-
/** Per-session stack of currently open
|
|
207
|
+
/** Per-session stack of currently open tool pipeline spans (for subagent parenting). */
|
|
208
208
|
const openToolSpans = new Map();
|
|
209
|
+
const toolCalls = new Map();
|
|
210
|
+
const toolResults = new WeakMap();
|
|
209
211
|
const openHooks = new Map();
|
|
210
212
|
const openOperations = new Set();
|
|
211
213
|
const traceContexts = new Map();
|
|
@@ -403,8 +405,11 @@ export function apply(ctx, config) {
|
|
|
403
405
|
metadata: { turn: data.turn, detachedHook: true },
|
|
404
406
|
})
|
|
405
407
|
: undefined;
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
+
const tool = data.callId ? toolCalls.get(sessionId)?.get(data.callId) : undefined;
|
|
409
|
+
const hookName = config.captureContent ? reporter.mask({ name: data.name }).name : undefined;
|
|
410
|
+
const span = reporter.startSpan(root === undefined ? (tool?.turn === data.turn ? tool.span : parentFor(sessionId)) : root, {
|
|
411
|
+
name: `hook:${data.point}${hookName ? `:${hookName}` : ''}`,
|
|
412
|
+
input: config.captureContent ? data.input : undefined,
|
|
408
413
|
metadata: {
|
|
409
414
|
turn: data.turn,
|
|
410
415
|
point: data.point,
|
|
@@ -412,6 +417,7 @@ export function apply(ctx, config) {
|
|
|
412
417
|
matcher: data.matcher,
|
|
413
418
|
handlerId: data.handlerId,
|
|
414
419
|
...(data.invocationId ? { invocationId: data.invocationId } : {}),
|
|
420
|
+
...(data.callId ? { callId: data.callId } : {}),
|
|
415
421
|
...(config.captureContent && data.name ? { name: data.name } : {}),
|
|
416
422
|
},
|
|
417
423
|
});
|
|
@@ -422,6 +428,7 @@ export function apply(ctx, config) {
|
|
|
422
428
|
point: data.point,
|
|
423
429
|
handlerId: data.handlerId,
|
|
424
430
|
persistent: !!data.invocationId,
|
|
431
|
+
callId: data.callId,
|
|
425
432
|
turnTrace: root === undefined ? (child?.turnTrace ?? sessions.get(sessionId)?.trace) : undefined,
|
|
426
433
|
});
|
|
427
434
|
openHooks.set(sessionId, pending);
|
|
@@ -437,7 +444,8 @@ export function apply(ctx, config) {
|
|
|
437
444
|
if (!hook ||
|
|
438
445
|
hook.handlerId !== data.handlerId ||
|
|
439
446
|
hook.turn !== data.turn ||
|
|
440
|
-
hook.point !== data.point
|
|
447
|
+
hook.point !== data.point ||
|
|
448
|
+
(data.callId !== undefined && hook.callId !== data.callId))
|
|
441
449
|
return;
|
|
442
450
|
const span = hook.span;
|
|
443
451
|
const failed = data.outcome
|
|
@@ -449,6 +457,21 @@ export function apply(ctx, config) {
|
|
|
449
457
|
? 'WARNING'
|
|
450
458
|
: 'DEFAULT';
|
|
451
459
|
if (span) {
|
|
460
|
+
const processOutput = {};
|
|
461
|
+
const truncation = {};
|
|
462
|
+
if (config.captureContent) {
|
|
463
|
+
const masked = reporter.mask({
|
|
464
|
+
stdout: data.stdout,
|
|
465
|
+
stderr: data.stderr,
|
|
466
|
+
stderrSummary: data.stderrSummary,
|
|
467
|
+
});
|
|
468
|
+
for (const [field, text] of Object.entries(masked)) {
|
|
469
|
+
if (text === undefined)
|
|
470
|
+
continue;
|
|
471
|
+
processOutput[field] = text.slice(0, 4096);
|
|
472
|
+
truncation[`${field}Truncated`] = text.length > 4096;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
452
475
|
if (failed && sessions.get(rootSessionId(sessionId) ?? '')?.trace === hook.turnTrace)
|
|
453
476
|
noteError(sessionId);
|
|
454
477
|
reporter.endSpan(span, {
|
|
@@ -457,10 +480,10 @@ export function apply(ctx, config) {
|
|
|
457
480
|
...(data.outcome ? { outcome: data.outcome } : {}),
|
|
458
481
|
...(data.signal ? { signal: data.signal } : {}),
|
|
459
482
|
exitCode: data.exitCode,
|
|
460
|
-
|
|
483
|
+
...(config.captureContent ? { result: data.result, ...processOutput } : {}),
|
|
461
484
|
},
|
|
462
485
|
level,
|
|
463
|
-
metadata: { durationMs: data.durationMs },
|
|
486
|
+
metadata: { durationMs: data.durationMs, ...truncation },
|
|
464
487
|
});
|
|
465
488
|
}
|
|
466
489
|
reporter.endSpan(hook.root ?? null, { level });
|
|
@@ -469,6 +492,8 @@ export function apply(ctx, config) {
|
|
|
469
492
|
openHooks.delete(sessionId);
|
|
470
493
|
return;
|
|
471
494
|
}
|
|
495
|
+
if (event.type === 'turn/end' || event.type === 'turn/start')
|
|
496
|
+
toolCalls.delete(sessionId);
|
|
472
497
|
if (event.type === 'turn/end')
|
|
473
498
|
closeHooks(sessionId, true);
|
|
474
499
|
const child = children.get(sessionId);
|
|
@@ -561,6 +586,7 @@ export function apply(ctx, config) {
|
|
|
561
586
|
sessions.delete(sessionId);
|
|
562
587
|
traceContexts.delete(sessionId);
|
|
563
588
|
openToolSpans.delete(sessionId);
|
|
589
|
+
toolCalls.delete(sessionId);
|
|
564
590
|
// End an abandoned root (crash-orphaned turn or between-turns one-off
|
|
565
591
|
// trace) — v5 spans only export on end, so dropping it open would lose
|
|
566
592
|
// everything it recorded.
|
|
@@ -756,12 +782,12 @@ export function apply(ctx, config) {
|
|
|
756
782
|
})();
|
|
757
783
|
});
|
|
758
784
|
// ------------------------------------------------------------------
|
|
759
|
-
// Span per tool
|
|
760
|
-
//
|
|
785
|
+
// Span per tool pipeline, including pre/post hooks and denied calls.
|
|
786
|
+
// Open spans are tracked per session
|
|
761
787
|
// so a subagent span opened mid-dispatch (foreground delegation) parents
|
|
762
788
|
// under the enclosing tool span.
|
|
763
789
|
// ------------------------------------------------------------------
|
|
764
|
-
ctx.on('tools/execute',
|
|
790
|
+
ctx.on('tools/pre-execute', (exec, next) => {
|
|
765
791
|
const sessionId = exec.agent?.id;
|
|
766
792
|
const parent = parentFor(sessionId);
|
|
767
793
|
// An agent-less dispatch's trace root has no session lifecycle to close
|
|
@@ -778,6 +804,12 @@ export function apply(ctx, config) {
|
|
|
778
804
|
});
|
|
779
805
|
if (sessionId && span) {
|
|
780
806
|
openToolSpans.set(sessionId, [...(openToolSpans.get(sessionId) ?? []), span]);
|
|
807
|
+
const calls = toolCalls.get(sessionId) ?? new Map();
|
|
808
|
+
calls.set(exec.callId, {
|
|
809
|
+
span,
|
|
810
|
+
turn: children.get(sessionId)?.turn ?? sessions.get(sessionId)?.turn,
|
|
811
|
+
});
|
|
812
|
+
toolCalls.set(sessionId, calls);
|
|
781
813
|
}
|
|
782
814
|
let closed = false;
|
|
783
815
|
const closeTool = () => finishTool({ level: 'WARNING', statusMessage: 'plugin unloaded during tool call' });
|
|
@@ -787,6 +819,13 @@ export function apply(ctx, config) {
|
|
|
787
819
|
closed = true;
|
|
788
820
|
const endedAt = new Date();
|
|
789
821
|
openOperations.delete(closeTool);
|
|
822
|
+
toolResults.delete(exec);
|
|
823
|
+
if (sessionId && toolCalls.get(sessionId)?.get(exec.callId)?.span === span) {
|
|
824
|
+
const calls = toolCalls.get(sessionId);
|
|
825
|
+
calls.delete(exec.callId);
|
|
826
|
+
if (!calls.size)
|
|
827
|
+
toolCalls.delete(sessionId);
|
|
828
|
+
}
|
|
790
829
|
if (sessionId && span) {
|
|
791
830
|
const remaining = (openToolSpans.get(sessionId) ?? []).filter((s) => s !== span);
|
|
792
831
|
if (remaining.length)
|
|
@@ -807,10 +846,9 @@ export function apply(ctx, config) {
|
|
|
807
846
|
end();
|
|
808
847
|
}
|
|
809
848
|
openOperations.add(closeTool);
|
|
810
|
-
|
|
811
|
-
const result = await next();
|
|
849
|
+
toolResults.set(exec, (result) => {
|
|
812
850
|
if (closed)
|
|
813
|
-
return
|
|
851
|
+
return;
|
|
814
852
|
if (result.isError)
|
|
815
853
|
noteError(sessionId);
|
|
816
854
|
const update = {
|
|
@@ -819,21 +857,18 @@ export function apply(ctx, config) {
|
|
|
819
857
|
statusMessage: result.isError ? contentMessage(config, result.error.message) : undefined,
|
|
820
858
|
metadata: { errorCode: result.isError ? result.error.info?.code : undefined },
|
|
821
859
|
};
|
|
822
|
-
if (span &&
|
|
860
|
+
if (span &&
|
|
861
|
+
config.captureContent &&
|
|
862
|
+
result.content.some((block) => block.type === 'image')) {
|
|
823
863
|
finishTool(update, displayBlocks(result.content, ctx.get('attachments'), mediaOptions, exec.signal));
|
|
824
864
|
}
|
|
825
865
|
else
|
|
826
866
|
finishTool(update);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
level: 'ERROR',
|
|
833
|
-
statusMessage: contentMessage(config, messageOf(error)),
|
|
834
|
-
});
|
|
835
|
-
throw error;
|
|
836
|
-
}
|
|
867
|
+
});
|
|
868
|
+
return next();
|
|
869
|
+
}, { prepend: true });
|
|
870
|
+
ctx.on('tools/result', (exec, result) => {
|
|
871
|
+
toolResults.get(exec)?.(result);
|
|
837
872
|
});
|
|
838
873
|
// Durability checkpoint: the store awaits every session/flush listener.
|
|
839
874
|
ctx.on('session/flush', async () => {
|
|
@@ -862,6 +897,7 @@ export function apply(ctx, config) {
|
|
|
862
897
|
traceContexts.clear();
|
|
863
898
|
children.clear();
|
|
864
899
|
openToolSpans.clear();
|
|
900
|
+
toolCalls.clear();
|
|
865
901
|
await reporter.shutdown();
|
|
866
902
|
};
|
|
867
903
|
});
|
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.24",
|
|
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",
|