@respan/cli 0.6.2 → 0.6.4

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.
@@ -721,17 +721,6 @@ function processChunk(hookData) {
721
721
  }
722
722
  }
723
723
  }
724
- const grounding = candidates[0].groundingMetadata ?? llmResp.groundingMetadata;
725
- if (grounding && typeof grounding === "object") {
726
- const queries = grounding.webSearchQueries ?? grounding.searchQueries ?? [];
727
- if (queries.length > 0) {
728
- chunkToolDetails.push({
729
- name: "google_web_search",
730
- args: { queries },
731
- output: truncate(queries.join(", "), MAX_CHARS)
732
- });
733
- }
734
- }
735
724
  }
736
725
  const messages = hookData.llm_request?.messages ?? [];
737
726
  const currentMsgCount = messages.length;
@@ -741,8 +730,17 @@ function processChunk(hookData) {
741
730
  let toolCallDetected = false;
742
731
  if (savedMsgCount > 0 && currentMsgCount > savedMsgCount) {
743
732
  const newMsgs = messages.slice(savedMsgCount);
744
- const hasNewUserMsg = newMsgs.some((m) => m.role === "user");
745
- if (hasNewUserMsg) {
733
+ const hasRealUserMsg = newMsgs.some((m) => {
734
+ if (String(m.role ?? "") !== "user") return false;
735
+ const parts = m.parts ?? m.content;
736
+ if (Array.isArray(parts)) {
737
+ return !parts.some(
738
+ (p) => typeof p === "object" && p !== null && (p.functionResponse || p.toolResponse)
739
+ );
740
+ }
741
+ return true;
742
+ });
743
+ if (hasRealUserMsg) {
746
744
  debug(`New user message detected (msgs ${savedMsgCount} \u2192 ${currentMsgCount}), starting fresh turn`);
747
745
  clearStreamState(sessionId);
748
746
  state = { accumulated_text: "", last_tokens: 0, first_chunk_time: "" };
@@ -760,13 +758,7 @@ function processChunk(hookData) {
760
758
  state.last_tokens = completionTokens || state.last_tokens;
761
759
  if (thoughtsTokens > 0) state.thoughts_tokens = thoughtsTokens;
762
760
  }
763
- const groundingDetails = chunkToolDetails.filter((d) => d.name === "google_web_search");
764
- if (groundingDetails.length) {
765
- state.tool_details = [...state.tool_details ?? [], ...groundingDetails];
766
- state.tool_turns = (state.tool_turns ?? 0) + groundingDetails.length;
767
- debug(`Grounding search detected: ${groundingDetails.length} queries`);
768
- }
769
- if (chunkText || groundingDetails.length) {
761
+ if (chunkText) {
770
762
  saveStreamState(sessionId, state);
771
763
  debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
772
764
  }
@@ -782,8 +774,10 @@ function processChunk(hookData) {
782
774
  process.stdout.write("{}\n");
783
775
  return;
784
776
  }
777
+ const hasPendingTools = (state.pending_tools ?? []).length > 0;
778
+ const hadToolsThisTurn = (state.tool_turns ?? 0) > 0 || hasPendingTools;
785
779
  const hasNewText = state.accumulated_text.length > (state.last_send_text_len ?? 0);
786
- const shouldSend = (!toolCallDetected || isFinished) && hasNewText && state.accumulated_text && (!chunkText || isFinished);
780
+ const shouldSend = hasNewText && state.accumulated_text && (isFinished || !hadToolsThisTurn && !toolCallDetected && !chunkText);
787
781
  process.stdout.write("{}\n");
788
782
  if (!shouldSend) {
789
783
  if (toolCallDetected) saveStreamState(sessionId, state);
@@ -449,18 +449,6 @@ function processChunk(hookData) {
449
449
  }
450
450
  }
451
451
  }
452
- // Detect server-side grounding (google_web_search) from groundingMetadata
453
- const grounding = (candidates[0].groundingMetadata ?? llmResp.groundingMetadata);
454
- if (grounding && typeof grounding === 'object') {
455
- const queries = (grounding.webSearchQueries ?? grounding.searchQueries ?? []);
456
- if (queries.length > 0) {
457
- chunkToolDetails.push({
458
- name: 'google_web_search',
459
- args: { queries },
460
- output: truncate(queries.join(', '), MAX_CHARS),
461
- });
462
- }
463
- }
464
452
  }
465
453
  const messages = (hookData.llm_request?.messages ?? []);
466
454
  const currentMsgCount = messages.length;
@@ -471,8 +459,19 @@ function processChunk(hookData) {
471
459
  let toolCallDetected = false;
472
460
  if (savedMsgCount > 0 && currentMsgCount > savedMsgCount) {
473
461
  const newMsgs = messages.slice(savedMsgCount);
474
- const hasNewUserMsg = newMsgs.some((m) => m.role === 'user');
475
- if (hasNewUserMsg) {
462
+ // Distinguish real user messages from tool-result messages injected by Gemini.
463
+ // Tool results contain functionResponse / toolResponse objects in their parts.
464
+ const hasRealUserMsg = newMsgs.some((m) => {
465
+ if (String(m.role ?? '') !== 'user')
466
+ return false;
467
+ const parts = (m.parts ?? m.content);
468
+ if (Array.isArray(parts)) {
469
+ // If any part has functionResponse/toolResponse, it's a tool result, not a user message
470
+ return !parts.some((p) => typeof p === 'object' && p !== null && (p.functionResponse || p.toolResponse));
471
+ }
472
+ return true; // plain text user message
473
+ });
474
+ if (hasRealUserMsg) {
476
475
  debug(`New user message detected (msgs ${savedMsgCount} → ${currentMsgCount}), starting fresh turn`);
477
476
  clearStreamState(sessionId);
478
477
  state = { accumulated_text: '', last_tokens: 0, first_chunk_time: '' };
@@ -494,14 +493,7 @@ function processChunk(hookData) {
494
493
  if (thoughtsTokens > 0)
495
494
  state.thoughts_tokens = thoughtsTokens;
496
495
  }
497
- // Save grounding tool details (these arrive with text, not as separate tool turns)
498
- const groundingDetails = chunkToolDetails.filter(d => d.name === 'google_web_search');
499
- if (groundingDetails.length) {
500
- state.tool_details = [...(state.tool_details ?? []), ...groundingDetails];
501
- state.tool_turns = (state.tool_turns ?? 0) + groundingDetails.length;
502
- debug(`Grounding search detected: ${groundingDetails.length} queries`);
503
- }
504
- if (chunkText || groundingDetails.length) {
496
+ if (chunkText) {
505
497
  saveStreamState(sessionId, state);
506
498
  debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
507
499
  }
@@ -518,12 +510,16 @@ function processChunk(hookData) {
518
510
  process.stdout.write('{}\n');
519
511
  return;
520
512
  }
521
- // Detect completion and send
513
+ // Detect completion and send.
514
+ // If tools have been used this turn, only send on STOP to avoid
515
+ // splitting a multi-tool turn into separate traces.
516
+ const hasPendingTools = (state.pending_tools ?? []).length > 0;
517
+ const hadToolsThisTurn = (state.tool_turns ?? 0) > 0 || hasPendingTools;
522
518
  const hasNewText = state.accumulated_text.length > (state.last_send_text_len ?? 0);
523
- const shouldSend = ((!toolCallDetected || isFinished)
524
- && hasNewText
519
+ const shouldSend = (hasNewText
525
520
  && state.accumulated_text
526
- && (!chunkText || isFinished));
521
+ && (isFinished
522
+ || (!hadToolsThisTurn && !toolCallDetected && !chunkText)));
527
523
  process.stdout.write('{}\n');
528
524
  if (!shouldSend) {
529
525
  if (toolCallDetected)
@@ -3459,5 +3459,5 @@
3459
3459
  ]
3460
3460
  }
3461
3461
  },
3462
- "version": "0.6.2"
3462
+ "version": "0.6.4"
3463
3463
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@respan/cli",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Respan CLI - manage your LLM observability from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",