@amaster.ai/dsh-langfuse 0.1.11 → 0.1.13
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 -0
- package/lib/index.js +65 -0
- package/lib/types/index.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@ Langfuse observability for [DeepSeek Harness (dsh)](https://github.com/deepseek-
|
|
|
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
8
|
- one **span** per tool call (`tools/execute` waterfall)
|
|
9
|
+
- one **span** per `hook/invoked` / `hook/result` pair, with hook point, decision, exit code, and duration (stderr summary follows `captureContent`)
|
|
9
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`
|
|
10
11
|
- subagent child sessions nested under the parent's tree (`session/created` header link + `subagent/start` / `subagent/end`)
|
|
11
12
|
|
|
@@ -38,6 +39,8 @@ Disabled by default. Configure via the profile's `cordis.patch.yml`:
|
|
|
38
39
|
|
|
39
40
|
Buffered spans drain on `session/flush(session)`; the exporter shuts down with the plugin fiber.
|
|
40
41
|
|
|
42
|
+
Hook spans appear when a dsh hook bridge emits the paired session records. The current `hook/result` record does not contain stdout; successful script output can appear in the LLM request when the bridge injects it as context, while the hook span reports decision, exit code, duration, and bounded stderr. `SessionStart` runs before the first turn and does not emit these records, so it has no hook span.
|
|
43
|
+
|
|
41
44
|
Observability is a no-throw seam: backend errors are logged with a `[dsh-langfuse]` prefix and never escape into the agent loop.
|
|
42
45
|
|
|
43
46
|
## Compatibility
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - `tools/execute` waterfall → one Langfuse **span** per tool dispatch
|
|
13
13
|
* (routed to the session's trace via `exec.agent.id` — the agent/session
|
|
14
14
|
* shared identity).
|
|
15
|
+
* - `hook/invoked` / `hook/result` session records → one **span** per hook run.
|
|
15
16
|
* - `session/event` emit → one Langfuse **trace** per agent turn
|
|
16
17
|
* (`turn/start` opens a root span — in the v5 SDK the root observation IS
|
|
17
18
|
* the trace — the turn's first `user/message` sets the input, `turn/end`
|
|
@@ -145,6 +146,13 @@ export function apply(ctx, config) {
|
|
|
145
146
|
const children = new Map();
|
|
146
147
|
/** Per-session stack of currently open tools/execute spans (for subagent parenting). */
|
|
147
148
|
const openToolSpans = new Map();
|
|
149
|
+
const openHooks = new Map();
|
|
150
|
+
function closeHooks(sessionId) {
|
|
151
|
+
for (const span of openHooks.get(sessionId)?.values() ?? []) {
|
|
152
|
+
reporter.endSpan(span, { level: 'WARNING', statusMessage: 'hook result not observed' });
|
|
153
|
+
}
|
|
154
|
+
openHooks.delete(sessionId);
|
|
155
|
+
}
|
|
148
156
|
/**
|
|
149
157
|
* The trace an observation for `sessionId` belongs to: the session's open
|
|
150
158
|
* turn trace, else a per-session one-off trace shared by that session's
|
|
@@ -230,6 +238,59 @@ export function apply(ctx, config) {
|
|
|
230
238
|
// ------------------------------------------------------------------
|
|
231
239
|
ctx.on('session/event', (session, event) => {
|
|
232
240
|
const sessionId = session.id;
|
|
241
|
+
const hookEvent = event;
|
|
242
|
+
if (hookEvent.type === 'hook/invoked') {
|
|
243
|
+
const data = hookEvent.data;
|
|
244
|
+
if (!data || typeof data.handlerId !== 'string' || typeof data.point !== 'string')
|
|
245
|
+
return;
|
|
246
|
+
const pending = openHooks.get(sessionId) ?? new Map();
|
|
247
|
+
if (pending.has(data.handlerId)) {
|
|
248
|
+
reporter.endSpan(pending.get(data.handlerId) ?? null, {
|
|
249
|
+
level: 'WARNING',
|
|
250
|
+
statusMessage: 'hook result not observed',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
pending.set(data.handlerId, reporter.startSpan(parentFor(sessionId), {
|
|
254
|
+
name: `hook:${data.point}`,
|
|
255
|
+
metadata: {
|
|
256
|
+
turn: data.turn,
|
|
257
|
+
point: data.point,
|
|
258
|
+
dialect: data.dialect,
|
|
259
|
+
matcher: data.matcher,
|
|
260
|
+
handlerId: data.handlerId,
|
|
261
|
+
},
|
|
262
|
+
}));
|
|
263
|
+
openHooks.set(sessionId, pending);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (hookEvent.type === 'hook/result') {
|
|
267
|
+
const data = hookEvent.data;
|
|
268
|
+
if (!data || typeof data.handlerId !== 'string' || typeof data.decision !== 'string')
|
|
269
|
+
return;
|
|
270
|
+
const pending = openHooks.get(sessionId);
|
|
271
|
+
const span = pending?.get(data.handlerId);
|
|
272
|
+
if (span) {
|
|
273
|
+
reporter.endSpan(span, {
|
|
274
|
+
output: {
|
|
275
|
+
decision: data.decision,
|
|
276
|
+
exitCode: data.exitCode,
|
|
277
|
+
stderrSummary: config.captureContent ? data.stderrSummary : undefined,
|
|
278
|
+
},
|
|
279
|
+
level: data.exitCode === undefined || (data.exitCode !== 0 && data.exitCode !== 2)
|
|
280
|
+
? 'ERROR'
|
|
281
|
+
: data.decision === 'block' || data.decision === 'deny' || data.decision === 'stop'
|
|
282
|
+
? 'WARNING'
|
|
283
|
+
: 'DEFAULT',
|
|
284
|
+
metadata: { durationMs: data.durationMs },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
pending?.delete(data.handlerId);
|
|
288
|
+
if (pending?.size === 0)
|
|
289
|
+
openHooks.delete(sessionId);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (event.type === 'turn/end')
|
|
293
|
+
closeHooks(sessionId);
|
|
233
294
|
const child = children.get(sessionId);
|
|
234
295
|
if (child) {
|
|
235
296
|
// The delegated prompt (the child's first user/message) is the
|
|
@@ -255,6 +316,7 @@ export function apply(ctx, config) {
|
|
|
255
316
|
// whose `turn/end` was never seen (crash-orphaned): end and replace
|
|
256
317
|
// it — v5 spans only export on end, so the stale root must close to
|
|
257
318
|
// keep what it already recorded.
|
|
319
|
+
closeHooks(sessionId);
|
|
258
320
|
const stale = sessions.get(sessionId);
|
|
259
321
|
if (stale)
|
|
260
322
|
reporter.endSpan(stale.trace);
|
|
@@ -299,6 +361,7 @@ export function apply(ctx, config) {
|
|
|
299
361
|
});
|
|
300
362
|
ctx.on('session/disposed', (session) => {
|
|
301
363
|
const sessionId = session.id;
|
|
364
|
+
closeHooks(sessionId);
|
|
302
365
|
const state = sessions.get(sessionId);
|
|
303
366
|
sessions.delete(sessionId);
|
|
304
367
|
openToolSpans.delete(sessionId);
|
|
@@ -518,6 +581,8 @@ export function apply(ctx, config) {
|
|
|
518
581
|
// down — v5 spans only export on end, so unloading mid-turn (profile
|
|
519
582
|
// reload) would otherwise silently drop them. (An in-flight generation
|
|
520
583
|
// isn't tracked; its own tee finally closes it if the stream settles.)
|
|
584
|
+
for (const sessionId of openHooks.keys())
|
|
585
|
+
closeHooks(sessionId);
|
|
521
586
|
for (const state of sessions.values())
|
|
522
587
|
reporter.endSpan(state.trace);
|
|
523
588
|
for (const child of children.values())
|
package/lib/types/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - `tools/execute` waterfall → one Langfuse **span** per tool dispatch
|
|
13
13
|
* (routed to the session's trace via `exec.agent.id` — the agent/session
|
|
14
14
|
* shared identity).
|
|
15
|
+
* - `hook/invoked` / `hook/result` session records → one **span** per hook run.
|
|
15
16
|
* - `session/event` emit → one Langfuse **trace** per agent turn
|
|
16
17
|
* (`turn/start` opens a root span — in the v5 SDK the root observation IS
|
|
17
18
|
* the trace — the turn's first `user/message` sets the input, `turn/end`
|
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.13",
|
|
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",
|