@amaster.ai/dsh-langfuse 0.1.10 → 0.1.12

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 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`
@@ -23,7 +24,7 @@
23
24
  * `subagent/descriptor` event).
24
25
  *
25
26
  * Event/waterfall shapes verified against the installed
26
- * @deepseek-ai/dsh-{llm,session,tools,agent,subagent}@0.1.6-alpha.2 package sources
27
+ * @deepseek-ai/dsh-{llm,session,tools,agent,subagent}@0.1.7-rc.2 package sources
27
28
  * (types for shapes; `lib/*.js` for the session/created ordering claim).
28
29
  *
29
30
  * @module dsh-langfuse
@@ -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())
@@ -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`
@@ -23,7 +24,7 @@
23
24
  * `subagent/descriptor` event).
24
25
  *
25
26
  * Event/waterfall shapes verified against the installed
26
- * @deepseek-ai/dsh-{llm,session,tools,agent,subagent}@0.1.6-alpha.2 package sources
27
+ * @deepseek-ai/dsh-{llm,session,tools,agent,subagent}@0.1.7-rc.2 package sources
27
28
  * (types for shapes; `lib/*.js` for the session/created ordering claim).
28
29
  *
29
30
  * @module dsh-langfuse
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/dsh-langfuse",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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",
@@ -36,10 +36,10 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 || >=0.1.6-alpha.1",
40
- "@deepseek-ai/dsh-session": ">=0.1.5-rc.1 || >=0.1.6-alpha.1",
41
- "@deepseek-ai/dsh-subagent": ">=0.1.5-rc.1 || >=0.1.6-alpha.1",
42
- "@deepseek-ai/dsh-tools": ">=0.1.5-rc.1 || >=0.1.6-alpha.1",
39
+ "@deepseek-ai/dsh-llm": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1",
40
+ "@deepseek-ai/dsh-session": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1",
41
+ "@deepseek-ai/dsh-subagent": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1",
42
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1",
43
43
  "@langfuse/otel": "^5.10.1",
44
44
  "@langfuse/tracing": "^5.10.1",
45
45
  "@opentelemetry/sdk-trace-node": "^2.10.0"
@@ -56,12 +56,15 @@
56
56
  }
57
57
  },
58
58
  "devDependencies": {
59
- "@deepseek-ai/cordis": "^4.0.1",
60
- "@deepseek-ai/dsh-agent": "0.1.6-alpha.2",
61
- "@deepseek-ai/dsh-llm": "0.1.6-alpha.2",
62
- "@deepseek-ai/dsh-session": "0.1.6-alpha.2",
63
- "@deepseek-ai/dsh-subagent": "0.1.6-alpha.2",
64
- "@deepseek-ai/dsh-tools": "0.1.6-alpha.2",
59
+ "@deepseek-ai/cordis": "^4.0.4",
60
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
62
+ "@deepseek-ai/dsh-ptc-runtime": "0.1.7-rc.2",
63
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
64
+ "@deepseek-ai/dsh-session-projection": "0.1.7-rc.2",
65
+ "@deepseek-ai/dsh-subagent": "0.1.7-rc.2",
66
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
67
+ "@deepseek-ai/dsh-util-time": "0.1.7-rc.2",
65
68
  "@langfuse/otel": "5.10.1",
66
69
  "@langfuse/tracing": "5.10.1",
67
70
  "@opentelemetry/api": "^1.9.1",