@respan/cli 0.6.1 → 0.6.3

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.
@@ -417,10 +417,20 @@ function clearStreamState(sessionId) {
417
417
  function extractMessages(hookData) {
418
418
  const llmReq = hookData.llm_request ?? {};
419
419
  const messages = llmReq.messages ?? [];
420
- return messages.map((msg) => ({
421
- role: String(msg.role ?? "user") === "model" ? "assistant" : String(msg.role ?? "user"),
422
- content: truncate(String(msg.content ?? ""), MAX_CHARS)
423
- }));
420
+ for (let i = messages.length - 1; i >= 0; i--) {
421
+ const role = String(messages[i].role ?? "user");
422
+ if (role === "user") {
423
+ return [{ role: "user", content: truncate(String(messages[i].content ?? ""), MAX_CHARS) }];
424
+ }
425
+ }
426
+ if (messages.length > 0) {
427
+ const last = messages[messages.length - 1];
428
+ return [{
429
+ role: String(last.role ?? "user") === "model" ? "assistant" : String(last.role ?? "user"),
430
+ content: truncate(String(last.content ?? ""), MAX_CHARS)
431
+ }];
432
+ }
433
+ return [];
424
434
  }
425
435
  function detectModel(hookData) {
426
436
  const override = process.env.RESPAN_GEMINI_MODEL;
@@ -720,8 +730,17 @@ function processChunk(hookData) {
720
730
  let toolCallDetected = false;
721
731
  if (savedMsgCount > 0 && currentMsgCount > savedMsgCount) {
722
732
  const newMsgs = messages.slice(savedMsgCount);
723
- const hasNewUserMsg = newMsgs.some((m) => m.role === "user");
724
- 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) {
725
744
  debug(`New user message detected (msgs ${savedMsgCount} \u2192 ${currentMsgCount}), starting fresh turn`);
726
745
  clearStreamState(sessionId);
727
746
  state = { accumulated_text: "", last_tokens: 0, first_chunk_time: "" };
@@ -738,6 +757,8 @@ function processChunk(hookData) {
738
757
  state.accumulated_text += chunkText;
739
758
  state.last_tokens = completionTokens || state.last_tokens;
740
759
  if (thoughtsTokens > 0) state.thoughts_tokens = thoughtsTokens;
760
+ }
761
+ if (chunkText) {
741
762
  saveStreamState(sessionId, state);
742
763
  debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
743
764
  }
@@ -118,10 +118,21 @@ function clearStreamState(sessionId) {
118
118
  function extractMessages(hookData) {
119
119
  const llmReq = (hookData.llm_request ?? {});
120
120
  const messages = (llmReq.messages ?? []);
121
- return messages.map((msg) => ({
122
- role: String(msg.role ?? 'user') === 'model' ? 'assistant' : String(msg.role ?? 'user'),
123
- content: truncate(String(msg.content ?? ''), MAX_CHARS),
124
- }));
121
+ // Only include the last user message, not the full conversation history
122
+ for (let i = messages.length - 1; i >= 0; i--) {
123
+ const role = String(messages[i].role ?? 'user');
124
+ if (role === 'user') {
125
+ return [{ role: 'user', content: truncate(String(messages[i].content ?? ''), MAX_CHARS) }];
126
+ }
127
+ }
128
+ if (messages.length > 0) {
129
+ const last = messages[messages.length - 1];
130
+ return [{
131
+ role: String(last.role ?? 'user') === 'model' ? 'assistant' : String(last.role ?? 'user'),
132
+ content: truncate(String(last.content ?? ''), MAX_CHARS),
133
+ }];
134
+ }
135
+ return [];
125
136
  }
126
137
  function detectModel(hookData) {
127
138
  const override = process.env.RESPAN_GEMINI_MODEL;
@@ -448,8 +459,19 @@ function processChunk(hookData) {
448
459
  let toolCallDetected = false;
449
460
  if (savedMsgCount > 0 && currentMsgCount > savedMsgCount) {
450
461
  const newMsgs = messages.slice(savedMsgCount);
451
- const hasNewUserMsg = newMsgs.some((m) => m.role === 'user');
452
- 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) {
453
475
  debug(`New user message detected (msgs ${savedMsgCount} → ${currentMsgCount}), starting fresh turn`);
454
476
  clearStreamState(sessionId);
455
477
  state = { accumulated_text: '', last_tokens: 0, first_chunk_time: '' };
@@ -462,7 +484,7 @@ function processChunk(hookData) {
462
484
  }
463
485
  }
464
486
  state.msg_count = currentMsgCount;
465
- // Accumulate text
487
+ // Accumulate text and grounding tool details
466
488
  if (chunkText) {
467
489
  if (!state.first_chunk_time)
468
490
  state.first_chunk_time = nowISO();
@@ -470,6 +492,8 @@ function processChunk(hookData) {
470
492
  state.last_tokens = completionTokens || state.last_tokens;
471
493
  if (thoughtsTokens > 0)
472
494
  state.thoughts_tokens = thoughtsTokens;
495
+ }
496
+ if (chunkText) {
473
497
  saveStreamState(sessionId, state);
474
498
  debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
475
499
  }