@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 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` waterfall)
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. 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.
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 hook. Unmatched/skipped hooks must not emit an invocation: these records represent actual execution. The producer bounds `stderrSummary` and omits unavailable optional fields rather than writing `undefined` into the JSON session log.
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. 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.
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 stderr 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).
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` waterfall → one Langfuse **span** per tool dispatch
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
- /** Per-session stack of currently open tools/execute spans (for subagent parenting). */
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 root = data.invocationId && !inTurn
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 span = reporter.startSpan(root === undefined ? parentFor(sessionId) : root, {
407
- name: `hook:${data.point}`,
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
- turnTrace: root === undefined ? (child?.turnTrace ?? sessions.get(sessionId)?.trace) : undefined,
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
- stderrSummary: config.captureContent ? data.stderrSummary : undefined,
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 dispatch. This waterfall IS async: `next()` returns the
760
- // normalized dispatch result promise. Open spans are tracked per session
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', async (exec, next) => {
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
- try {
811
- const result = await next();
879
+ toolResults.set(exec, (result) => {
812
880
  if (closed)
813
- return result;
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 && config.captureContent && result.content.some((block) => block.type === 'image')) {
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
- return result;
828
- }
829
- catch (error) {
830
- noteError(sessionId);
831
- finishTool({
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
  });
@@ -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;
@@ -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` waterfall → one Langfuse **span** per tool dispatch
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.23",
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",