@respan/cli 0.6.8 → 0.7.0

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.
@@ -340,7 +340,7 @@ function toOtlpPayload(spans) {
340
340
  })
341
341
  },
342
342
  scopeSpans: [{
343
- scope: { name: "respan-cli-hooks", version: "0.5.3" },
343
+ scope: { name: "respan-cli-hooks", version: "0.7.0" },
344
344
  spans: otlpSpans
345
345
  }]
346
346
  }]
@@ -339,7 +339,7 @@ function toOtlpPayload(spans) {
339
339
  })
340
340
  },
341
341
  scopeSpans: [{
342
- scope: { name: "respan-cli-hooks", version: "0.5.3" },
342
+ scope: { name: "respan-cli-hooks", version: "0.7.0" },
343
343
  spans: otlpSpans
344
344
  }]
345
345
  }]
@@ -318,7 +318,7 @@ function toOtlpPayload(spans) {
318
318
  })
319
319
  },
320
320
  scopeSpans: [{
321
- scope: { name: "respan-cli-hooks", version: "0.5.3" },
321
+ scope: { name: "respan-cli-hooks", version: "0.7.0" },
322
322
  spans: otlpSpans
323
323
  }]
324
324
  }]
@@ -438,7 +438,7 @@ function detectModel(hookData) {
438
438
  const llmReq = hookData.llm_request ?? {};
439
439
  return String(llmReq.model ?? "") || "gemini-cli";
440
440
  }
441
- function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens) {
441
+ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens, textRounds, roundStartTimes) {
442
442
  const spans = [];
443
443
  const sessionId = String(hookData.session_id ?? "");
444
444
  const model = detectModel(hookData);
@@ -447,7 +447,6 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
447
447
  const beginTime = startTimeIso || endTime;
448
448
  const lat = latencySeconds(beginTime, endTime);
449
449
  const promptMessages = extractMessages(hookData);
450
- const completionMessage = { role: "assistant", content: truncate(outputText, MAX_CHARS) };
451
450
  const { workflowName, spanName, customerId } = resolveSpanFields(config, {
452
451
  workflowName: "gemini-cli",
453
452
  spanName: "gemini-cli"
@@ -480,50 +479,85 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
480
479
  metadata,
481
480
  ...lat !== void 0 ? { latency: lat } : {}
482
481
  });
483
- const genSpan = {
484
- trace_unique_id: traceUniqueId,
485
- span_unique_id: `gcli_${safeId}_${turnTs}_gen`,
486
- span_parent_id: rootSpanId,
487
- span_name: "gemini.chat",
488
- span_workflow_name: workflowName,
489
- span_path: "gemini_chat",
490
- model,
491
- provider_id: "google",
492
- metadata: {},
493
- input: promptMessages.length ? JSON.stringify(promptMessages) : "",
494
- output: truncate(outputText, MAX_CHARS),
495
- timestamp: endTime,
496
- start_time: beginTime,
497
- prompt_tokens: tokens.prompt_tokens,
498
- completion_tokens: tokens.completion_tokens,
499
- total_tokens: tokens.total_tokens,
500
- ...lat !== void 0 ? { latency: lat } : {}
501
- };
502
- if (reqConfig.temperature != null) genSpan.temperature = reqConfig.temperature;
503
- if (reqConfig.maxOutputTokens != null) genSpan.max_tokens = reqConfig.maxOutputTokens;
504
- spans.push(genSpan);
505
- if (thoughtsTokens > 0) {
506
- spans.push({
507
- trace_unique_id: traceUniqueId,
508
- span_unique_id: `gcli_${safeId}_${turnTs}_reasoning`,
509
- span_parent_id: rootSpanId,
510
- span_name: "Reasoning",
511
- span_workflow_name: workflowName,
512
- span_path: "reasoning",
513
- provider_id: "",
514
- metadata: { reasoning_tokens: thoughtsTokens },
515
- input: "",
516
- output: `[Reasoning: ${thoughtsTokens} tokens]`,
517
- timestamp: endTime,
518
- start_time: beginTime
519
- });
482
+ const rounds = textRounds.length > 0 ? textRounds : [outputText];
483
+ const roundStarts = roundStartTimes.length > 0 ? roundStartTimes : [beginTime];
484
+ let toolIdx = 0;
485
+ for (let r = 0; r < rounds.length; r++) {
486
+ const roundText = rounds[r];
487
+ const roundStart = roundStarts[r] || beginTime;
488
+ const nextTool = toolIdx < toolDetails.length ? toolDetails[toolIdx] : null;
489
+ const roundEnd = r < rounds.length - 1 && nextTool?.start_time ? nextTool.start_time : endTime;
490
+ const roundLat = latencySeconds(roundStart, roundEnd);
491
+ if (roundText) {
492
+ const genSpan = {
493
+ trace_unique_id: traceUniqueId,
494
+ span_unique_id: `gcli_${safeId}_${turnTs}_gen_${r}`,
495
+ span_parent_id: rootSpanId,
496
+ span_name: "gemini.chat",
497
+ span_workflow_name: workflowName,
498
+ span_path: "gemini_chat",
499
+ model,
500
+ provider_id: "google",
501
+ metadata: {},
502
+ input: r === 0 && promptMessages.length ? JSON.stringify(promptMessages) : "",
503
+ output: truncate(roundText, MAX_CHARS),
504
+ timestamp: roundEnd,
505
+ start_time: roundStart,
506
+ ...roundLat !== void 0 ? { latency: roundLat } : {},
507
+ // Only attach tokens to the first round (aggregate usage from Gemini)
508
+ ...r === 0 ? {
509
+ prompt_tokens: tokens.prompt_tokens,
510
+ completion_tokens: tokens.completion_tokens,
511
+ total_tokens: tokens.total_tokens
512
+ } : {}
513
+ };
514
+ if (r === 0) {
515
+ if (reqConfig.temperature != null) genSpan.temperature = reqConfig.temperature;
516
+ if (reqConfig.maxOutputTokens != null) genSpan.max_tokens = reqConfig.maxOutputTokens;
517
+ }
518
+ spans.push(genSpan);
519
+ }
520
+ if (r < rounds.length - 1) {
521
+ while (toolIdx < toolDetails.length) {
522
+ const detail = toolDetails[toolIdx];
523
+ const toolName = detail?.name ?? "";
524
+ const toolArgs = detail?.args ?? detail?.input ?? {};
525
+ const toolOutput = detail?.output ?? "";
526
+ const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
527
+ const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
528
+ const toolMeta = {};
529
+ if (toolName) toolMeta.tool_name = toolName;
530
+ if (detail?.error) toolMeta.error = detail.error;
531
+ const toolStart = detail?.start_time ?? beginTime;
532
+ const toolEnd = detail?.end_time ?? endTime;
533
+ const toolLat = latencySeconds(toolStart, toolEnd);
534
+ spans.push({
535
+ trace_unique_id: traceUniqueId,
536
+ span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
537
+ span_parent_id: rootSpanId,
538
+ span_name: `Tool: ${displayName}`,
539
+ span_workflow_name: workflowName,
540
+ span_path: toolName ? `tool_${toolName}` : "tool_call",
541
+ provider_id: "",
542
+ metadata: toolMeta,
543
+ input: toolInputStr,
544
+ output: truncate(toolOutput, MAX_CHARS),
545
+ timestamp: toolEnd,
546
+ start_time: toolStart,
547
+ ...toolLat !== void 0 ? { latency: toolLat } : {}
548
+ });
549
+ toolIdx++;
550
+ const nextDetail = toolDetails[toolIdx];
551
+ if (nextDetail && roundStarts[r + 1] && nextDetail.start_time && nextDetail.start_time > roundStarts[r + 1]) break;
552
+ }
553
+ }
520
554
  }
521
- for (let i = 0; i < toolTurns; i++) {
522
- const detail = toolDetails[i] ?? null;
555
+ while (toolIdx < toolDetails.length) {
556
+ const detail = toolDetails[toolIdx];
523
557
  const toolName = detail?.name ?? "";
524
558
  const toolArgs = detail?.args ?? detail?.input ?? {};
525
559
  const toolOutput = detail?.output ?? "";
526
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${i + 1}`;
560
+ const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
527
561
  const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
528
562
  const toolMeta = {};
529
563
  if (toolName) toolMeta.tool_name = toolName;
@@ -533,7 +567,7 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
533
567
  const toolLat = latencySeconds(toolStart, toolEnd);
534
568
  spans.push({
535
569
  trace_unique_id: traceUniqueId,
536
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${i + 1}`,
570
+ span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
537
571
  span_parent_id: rootSpanId,
538
572
  span_name: `Tool: ${displayName}`,
539
573
  span_workflow_name: workflowName,
@@ -546,6 +580,23 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
546
580
  start_time: toolStart,
547
581
  ...toolLat !== void 0 ? { latency: toolLat } : {}
548
582
  });
583
+ toolIdx++;
584
+ }
585
+ if (thoughtsTokens > 0) {
586
+ spans.push({
587
+ trace_unique_id: traceUniqueId,
588
+ span_unique_id: `gcli_${safeId}_${turnTs}_reasoning`,
589
+ span_parent_id: rootSpanId,
590
+ span_name: "Reasoning",
591
+ span_workflow_name: workflowName,
592
+ span_path: "reasoning",
593
+ provider_id: "",
594
+ metadata: { reasoning_tokens: thoughtsTokens },
595
+ input: "",
596
+ output: `[Reasoning: ${thoughtsTokens} tokens]`,
597
+ timestamp: endTime,
598
+ start_time: beginTime
599
+ });
549
600
  }
550
601
  return addDefaultsToAll(spans);
551
602
  }
@@ -665,7 +716,6 @@ function processBeforeTool(hookData) {
665
716
  pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
666
717
  state.pending_tools = pending;
667
718
  state.send_version = (state.send_version ?? 0) + 1;
668
- state.tool_turns = (state.tool_turns ?? 0) + 1;
669
719
  saveStreamState(sessionId, state);
670
720
  }
671
721
  function processAfterTool(hookData) {
@@ -748,7 +798,8 @@ function processChunk(hookData) {
748
798
  state.tool_turns = (state.tool_turns ?? 0) + 1;
749
799
  state.send_version = (state.send_version ?? 0) + 1;
750
800
  toolCallDetected = true;
751
- debug(`Tool call detected via msg_count (${savedMsgCount} \u2192 ${currentMsgCount}), tool_turns=${state.tool_turns}`);
801
+ state.current_round = (state.current_round ?? 0) + 1;
802
+ debug(`Tool call detected via msg_count (${savedMsgCount} \u2192 ${currentMsgCount}), tool_turns=${state.tool_turns}, round=${state.current_round}`);
752
803
  }
753
804
  }
754
805
  state.msg_count = currentMsgCount;
@@ -757,10 +808,15 @@ function processChunk(hookData) {
757
808
  state.accumulated_text += chunkText;
758
809
  state.last_tokens = completionTokens || state.last_tokens;
759
810
  if (thoughtsTokens > 0) state.thoughts_tokens = thoughtsTokens;
760
- }
761
- if (chunkText) {
811
+ const round = state.current_round ?? 0;
812
+ if (!state.text_rounds) state.text_rounds = [];
813
+ if (!state.round_start_times) state.round_start_times = [];
814
+ while (state.text_rounds.length <= round) state.text_rounds.push("");
815
+ while (state.round_start_times.length <= round) state.round_start_times.push("");
816
+ state.text_rounds[round] += chunkText;
817
+ if (!state.round_start_times[round]) state.round_start_times[round] = nowISO();
762
818
  saveStreamState(sessionId, state);
763
- debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
819
+ debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}, round=${round}`);
764
820
  }
765
821
  const isToolTurn = hasToolCall || ["TOOL_CALLS", "FUNCTION_CALL", "TOOL_USE"].includes(finishReason);
766
822
  if (isToolTurn) {
@@ -800,7 +856,9 @@ function processChunk(hookData) {
800
856
  state.first_chunk_time || void 0,
801
857
  state.tool_turns ?? 0,
802
858
  state.tool_details ?? [],
803
- state.thoughts_tokens ?? 0
859
+ state.thoughts_tokens ?? 0,
860
+ state.text_rounds ?? [],
861
+ state.round_start_times ?? []
804
862
  );
805
863
  if (isFinished && chunkText) {
806
864
  debug(`Immediate send (text+STOP, tool_turns=${state.tool_turns ?? 0}), ${state.accumulated_text.length} chars`);
@@ -814,35 +872,30 @@ function processChunk(hookData) {
814
872
  debug(`Delayed send (version=${state.send_version}, delay=${SEND_DELAY}s), ${state.accumulated_text.length} chars`);
815
873
  launchDelayedSend(sessionId, state.send_version, spans, creds.apiKey, creds.baseUrl);
816
874
  }
817
- function mainWorker(raw) {
875
+ function processChunkInWorker(dataFile) {
818
876
  try {
877
+ const raw = fs2.readFileSync(dataFile, "utf-8");
878
+ fs2.unlinkSync(dataFile);
819
879
  if (!raw.trim()) return;
820
880
  const hookData = JSON.parse(raw);
821
- const event = String(hookData.hook_event_name ?? "");
822
881
  const unlock = acquireLock(LOCK_PATH);
823
882
  try {
824
- if (event === "BeforeTool") {
825
- processBeforeTool(hookData);
826
- } else if (event === "AfterTool") {
827
- processAfterTool(hookData);
828
- } else {
829
- processChunk(hookData);
830
- }
883
+ processChunk(hookData);
831
884
  } finally {
832
885
  unlock?.();
833
886
  }
834
887
  } catch (e) {
835
- if (e instanceof SyntaxError) {
836
- log("ERROR", `Invalid JSON from stdin: ${e}`);
837
- } else {
838
- log("ERROR", `Hook error: ${e}`);
888
+ log("ERROR", `Worker error: ${e}`);
889
+ try {
890
+ fs2.unlinkSync(dataFile);
891
+ } catch {
839
892
  }
840
893
  }
841
894
  }
842
895
  function main() {
843
896
  if (process.env._RESPAN_GEM_WORKER === "1") {
844
- const raw2 = process.env._RESPAN_GEM_DATA ?? "";
845
- mainWorker(raw2);
897
+ const dataFile = process.env._RESPAN_GEM_FILE ?? "";
898
+ if (dataFile) processChunkInWorker(dataFile);
846
899
  return;
847
900
  }
848
901
  let raw = "";
@@ -855,15 +908,34 @@ function main() {
855
908
  process.exit(0);
856
909
  }
857
910
  try {
858
- const scriptPath = __filename || process.argv[1];
859
- const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
860
- env: { ...process.env, _RESPAN_GEM_WORKER: "1", _RESPAN_GEM_DATA: raw },
861
- stdio: "ignore",
862
- detached: true
863
- });
864
- child.unref();
911
+ const hookData = JSON.parse(raw);
912
+ const event = String(hookData.hook_event_name ?? "");
913
+ if (event === "BeforeTool" || event === "AfterTool") {
914
+ const unlock = acquireLock(LOCK_PATH);
915
+ try {
916
+ if (event === "BeforeTool") processBeforeTool(hookData);
917
+ else processAfterTool(hookData);
918
+ } finally {
919
+ unlock?.();
920
+ }
921
+ } else {
922
+ const dataFile = path2.join(STATE_DIR, `respan_chunk_${process.pid}.json`);
923
+ fs2.mkdirSync(STATE_DIR, { recursive: true });
924
+ fs2.writeFileSync(dataFile, raw);
925
+ try {
926
+ const scriptPath = __filename || process.argv[1];
927
+ const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
928
+ env: { ...process.env, _RESPAN_GEM_WORKER: "1", _RESPAN_GEM_FILE: dataFile },
929
+ stdio: "ignore",
930
+ detached: true
931
+ });
932
+ child.unref();
933
+ } catch (e) {
934
+ processChunkInWorker(dataFile);
935
+ }
936
+ }
865
937
  } catch (e) {
866
- mainWorker(raw);
938
+ log("ERROR", `Hook error: ${e}`);
867
939
  }
868
940
  process.exit(0);
869
941
  }