@respan/cli 0.6.7 → 0.6.9
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/dist/hooks/gemini-cli.cjs +109 -48
- package/dist/hooks/gemini-cli.js +132 -57
- package/oclif.manifest.json +678 -678
- package/package.json +1 -1
|
@@ -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
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
522
|
-
const detail = toolDetails[
|
|
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 ${
|
|
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_${
|
|
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
|
}
|
|
@@ -664,6 +715,8 @@ function processBeforeTool(hookData) {
|
|
|
664
715
|
const pending = state.pending_tools ?? [];
|
|
665
716
|
pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
|
|
666
717
|
state.pending_tools = pending;
|
|
718
|
+
state.send_version = (state.send_version ?? 0) + 1;
|
|
719
|
+
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
667
720
|
saveStreamState(sessionId, state);
|
|
668
721
|
}
|
|
669
722
|
function processAfterTool(hookData) {
|
|
@@ -746,7 +799,8 @@ function processChunk(hookData) {
|
|
|
746
799
|
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
747
800
|
state.send_version = (state.send_version ?? 0) + 1;
|
|
748
801
|
toolCallDetected = true;
|
|
749
|
-
|
|
802
|
+
state.current_round = (state.current_round ?? 0) + 1;
|
|
803
|
+
debug(`Tool call detected via msg_count (${savedMsgCount} \u2192 ${currentMsgCount}), tool_turns=${state.tool_turns}, round=${state.current_round}`);
|
|
750
804
|
}
|
|
751
805
|
}
|
|
752
806
|
state.msg_count = currentMsgCount;
|
|
@@ -755,10 +809,15 @@ function processChunk(hookData) {
|
|
|
755
809
|
state.accumulated_text += chunkText;
|
|
756
810
|
state.last_tokens = completionTokens || state.last_tokens;
|
|
757
811
|
if (thoughtsTokens > 0) state.thoughts_tokens = thoughtsTokens;
|
|
758
|
-
|
|
759
|
-
|
|
812
|
+
const round = state.current_round ?? 0;
|
|
813
|
+
if (!state.text_rounds) state.text_rounds = [];
|
|
814
|
+
if (!state.round_start_times) state.round_start_times = [];
|
|
815
|
+
while (state.text_rounds.length <= round) state.text_rounds.push("");
|
|
816
|
+
while (state.round_start_times.length <= round) state.round_start_times.push("");
|
|
817
|
+
state.text_rounds[round] += chunkText;
|
|
818
|
+
if (!state.round_start_times[round]) state.round_start_times[round] = nowISO();
|
|
760
819
|
saveStreamState(sessionId, state);
|
|
761
|
-
debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
|
|
820
|
+
debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}, round=${round}`);
|
|
762
821
|
}
|
|
763
822
|
const isToolTurn = hasToolCall || ["TOOL_CALLS", "FUNCTION_CALL", "TOOL_USE"].includes(finishReason);
|
|
764
823
|
if (isToolTurn) {
|
|
@@ -798,7 +857,9 @@ function processChunk(hookData) {
|
|
|
798
857
|
state.first_chunk_time || void 0,
|
|
799
858
|
state.tool_turns ?? 0,
|
|
800
859
|
state.tool_details ?? [],
|
|
801
|
-
state.thoughts_tokens ?? 0
|
|
860
|
+
state.thoughts_tokens ?? 0,
|
|
861
|
+
state.text_rounds ?? [],
|
|
862
|
+
state.round_start_times ?? []
|
|
802
863
|
);
|
|
803
864
|
if (isFinished && chunkText) {
|
|
804
865
|
debug(`Immediate send (text+STOP, tool_turns=${state.tool_turns ?? 0}), ${state.accumulated_text.length} chars`);
|
package/dist/hooks/gemini-cli.js
CHANGED
|
@@ -142,7 +142,7 @@ function detectModel(hookData) {
|
|
|
142
142
|
return String(llmReq.model ?? '') || 'gemini-cli';
|
|
143
143
|
}
|
|
144
144
|
// ── Span construction ─────────────────────────────────────────────
|
|
145
|
-
function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens) {
|
|
145
|
+
function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens, textRounds, roundStartTimes) {
|
|
146
146
|
const spans = [];
|
|
147
147
|
const sessionId = String(hookData.session_id ?? '');
|
|
148
148
|
const model = detectModel(hookData);
|
|
@@ -151,21 +151,17 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
|
|
|
151
151
|
const beginTime = startTimeIso || endTime;
|
|
152
152
|
const lat = latencySeconds(beginTime, endTime);
|
|
153
153
|
const promptMessages = extractMessages(hookData);
|
|
154
|
-
const completionMessage = { role: 'assistant', content: truncate(outputText, MAX_CHARS) };
|
|
155
154
|
const { workflowName, spanName, customerId } = resolveSpanFields(config, {
|
|
156
155
|
workflowName: 'gemini-cli',
|
|
157
156
|
spanName: 'gemini-cli',
|
|
158
157
|
});
|
|
159
158
|
const safeId = sessionId.replace(/[/\\]/g, '_').slice(0, 50);
|
|
160
|
-
// Use first chunk timestamp to differentiate turns within the same session
|
|
161
159
|
const turnTs = beginTime.replace(/[^0-9]/g, '').slice(0, 14);
|
|
162
160
|
const traceUniqueId = `gcli_${safeId}_${turnTs}`;
|
|
163
161
|
const rootSpanId = `gcli_${safeId}_${turnTs}_root`;
|
|
164
162
|
const threadId = `gcli_${sessionId}`;
|
|
165
|
-
// LLM config
|
|
166
163
|
const llmReq = (hookData.llm_request ?? {});
|
|
167
164
|
const reqConfig = (llmReq.config ?? {});
|
|
168
|
-
// Metadata
|
|
169
165
|
const baseMeta = { source: 'gemini-cli' };
|
|
170
166
|
if (toolTurns > 0)
|
|
171
167
|
baseMeta.tool_turns = toolTurns;
|
|
@@ -190,55 +186,99 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
|
|
|
190
186
|
metadata,
|
|
191
187
|
...(lat !== undefined ? { latency: lat } : {}),
|
|
192
188
|
});
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
189
|
+
// Build interleaved LLM + Tool spans in chronological order.
|
|
190
|
+
// If we have text rounds, create one gemini.chat per round with tools between them.
|
|
191
|
+
// Otherwise fall back to a single gemini.chat span.
|
|
192
|
+
const rounds = textRounds.length > 0 ? textRounds : [outputText];
|
|
193
|
+
const roundStarts = roundStartTimes.length > 0 ? roundStartTimes : [beginTime];
|
|
194
|
+
let toolIdx = 0;
|
|
195
|
+
for (let r = 0; r < rounds.length; r++) {
|
|
196
|
+
const roundText = rounds[r];
|
|
197
|
+
const roundStart = roundStarts[r] || beginTime;
|
|
198
|
+
// Round end: next tool start, or endTime for last round
|
|
199
|
+
const nextTool = toolIdx < toolDetails.length ? toolDetails[toolIdx] : null;
|
|
200
|
+
const roundEnd = (r < rounds.length - 1 && nextTool?.start_time) ? nextTool.start_time : endTime;
|
|
201
|
+
const roundLat = latencySeconds(roundStart, roundEnd);
|
|
202
|
+
// LLM generation span for this round
|
|
203
|
+
if (roundText) {
|
|
204
|
+
const genSpan = {
|
|
205
|
+
trace_unique_id: traceUniqueId,
|
|
206
|
+
span_unique_id: `gcli_${safeId}_${turnTs}_gen_${r}`,
|
|
207
|
+
span_parent_id: rootSpanId,
|
|
208
|
+
span_name: 'gemini.chat',
|
|
209
|
+
span_workflow_name: workflowName,
|
|
210
|
+
span_path: 'gemini_chat',
|
|
211
|
+
model,
|
|
212
|
+
provider_id: 'google',
|
|
213
|
+
metadata: {},
|
|
214
|
+
input: r === 0 && promptMessages.length ? JSON.stringify(promptMessages) : '',
|
|
215
|
+
output: truncate(roundText, MAX_CHARS),
|
|
216
|
+
timestamp: roundEnd,
|
|
217
|
+
start_time: roundStart,
|
|
218
|
+
...(roundLat !== undefined ? { latency: roundLat } : {}),
|
|
219
|
+
// Only attach tokens to the first round (aggregate usage from Gemini)
|
|
220
|
+
...(r === 0 ? {
|
|
221
|
+
prompt_tokens: tokens.prompt_tokens,
|
|
222
|
+
completion_tokens: tokens.completion_tokens,
|
|
223
|
+
total_tokens: tokens.total_tokens,
|
|
224
|
+
} : {}),
|
|
225
|
+
};
|
|
226
|
+
if (r === 0) {
|
|
227
|
+
if (reqConfig.temperature != null)
|
|
228
|
+
genSpan.temperature = reqConfig.temperature;
|
|
229
|
+
if (reqConfig.maxOutputTokens != null)
|
|
230
|
+
genSpan.max_tokens = reqConfig.maxOutputTokens;
|
|
231
|
+
}
|
|
232
|
+
spans.push(genSpan);
|
|
233
|
+
}
|
|
234
|
+
// Tool spans that come after this round (before next round)
|
|
235
|
+
if (r < rounds.length - 1) {
|
|
236
|
+
// Emit all tools between this round and the next
|
|
237
|
+
while (toolIdx < toolDetails.length) {
|
|
238
|
+
const detail = toolDetails[toolIdx];
|
|
239
|
+
const toolName = detail?.name ?? '';
|
|
240
|
+
const toolArgs = detail?.args ?? detail?.input ?? {};
|
|
241
|
+
const toolOutput = detail?.output ?? '';
|
|
242
|
+
const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
|
|
243
|
+
const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
|
|
244
|
+
const toolMeta = {};
|
|
245
|
+
if (toolName)
|
|
246
|
+
toolMeta.tool_name = toolName;
|
|
247
|
+
if (detail?.error)
|
|
248
|
+
toolMeta.error = detail.error;
|
|
249
|
+
const toolStart = detail?.start_time ?? beginTime;
|
|
250
|
+
const toolEnd = detail?.end_time ?? endTime;
|
|
251
|
+
const toolLat = latencySeconds(toolStart, toolEnd);
|
|
252
|
+
spans.push({
|
|
253
|
+
trace_unique_id: traceUniqueId,
|
|
254
|
+
span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
|
|
255
|
+
span_parent_id: rootSpanId,
|
|
256
|
+
span_name: `Tool: ${displayName}`,
|
|
257
|
+
span_workflow_name: workflowName,
|
|
258
|
+
span_path: toolName ? `tool_${toolName}` : 'tool_call',
|
|
259
|
+
provider_id: '',
|
|
260
|
+
metadata: toolMeta,
|
|
261
|
+
input: toolInputStr,
|
|
262
|
+
output: truncate(toolOutput, MAX_CHARS),
|
|
263
|
+
timestamp: toolEnd,
|
|
264
|
+
start_time: toolStart,
|
|
265
|
+
...(toolLat !== undefined ? { latency: toolLat } : {}),
|
|
266
|
+
});
|
|
267
|
+
toolIdx++;
|
|
268
|
+
// If next tool starts after next round's start time, break — it belongs to a later gap
|
|
269
|
+
const nextDetail = toolDetails[toolIdx];
|
|
270
|
+
if (nextDetail && roundStarts[r + 1] && nextDetail.start_time && nextDetail.start_time > roundStarts[r + 1])
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
234
274
|
}
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
const detail = toolDetails[
|
|
275
|
+
// Any remaining tools not yet emitted (e.g. only one round but tools exist)
|
|
276
|
+
while (toolIdx < toolDetails.length) {
|
|
277
|
+
const detail = toolDetails[toolIdx];
|
|
238
278
|
const toolName = detail?.name ?? '';
|
|
239
279
|
const toolArgs = detail?.args ?? detail?.input ?? {};
|
|
240
280
|
const toolOutput = detail?.output ?? '';
|
|
241
|
-
const displayName = toolName ? toolDisplayName(toolName) : `Call ${
|
|
281
|
+
const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
|
|
242
282
|
const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
|
|
243
283
|
const toolMeta = {};
|
|
244
284
|
if (toolName)
|
|
@@ -250,7 +290,7 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
|
|
|
250
290
|
const toolLat = latencySeconds(toolStart, toolEnd);
|
|
251
291
|
spans.push({
|
|
252
292
|
trace_unique_id: traceUniqueId,
|
|
253
|
-
span_unique_id: `gcli_${safeId}_${turnTs}_tool_${
|
|
293
|
+
span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
|
|
254
294
|
span_parent_id: rootSpanId,
|
|
255
295
|
span_name: `Tool: ${displayName}`,
|
|
256
296
|
span_workflow_name: workflowName,
|
|
@@ -263,6 +303,24 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
|
|
|
263
303
|
start_time: toolStart,
|
|
264
304
|
...(toolLat !== undefined ? { latency: toolLat } : {}),
|
|
265
305
|
});
|
|
306
|
+
toolIdx++;
|
|
307
|
+
}
|
|
308
|
+
// Reasoning span
|
|
309
|
+
if (thoughtsTokens > 0) {
|
|
310
|
+
spans.push({
|
|
311
|
+
trace_unique_id: traceUniqueId,
|
|
312
|
+
span_unique_id: `gcli_${safeId}_${turnTs}_reasoning`,
|
|
313
|
+
span_parent_id: rootSpanId,
|
|
314
|
+
span_name: 'Reasoning',
|
|
315
|
+
span_workflow_name: workflowName,
|
|
316
|
+
span_path: 'reasoning',
|
|
317
|
+
provider_id: '',
|
|
318
|
+
metadata: { reasoning_tokens: thoughtsTokens },
|
|
319
|
+
input: '',
|
|
320
|
+
output: `[Reasoning: ${thoughtsTokens} tokens]`,
|
|
321
|
+
timestamp: endTime,
|
|
322
|
+
start_time: beginTime,
|
|
323
|
+
});
|
|
266
324
|
}
|
|
267
325
|
return addDefaultsToAll(spans);
|
|
268
326
|
}
|
|
@@ -387,6 +445,10 @@ function processBeforeTool(hookData) {
|
|
|
387
445
|
const pending = state.pending_tools ?? [];
|
|
388
446
|
pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
|
|
389
447
|
state.pending_tools = pending;
|
|
448
|
+
// Increment send_version to cancel any pending delayed sends —
|
|
449
|
+
// the turn isn't done yet, a tool is about to execute.
|
|
450
|
+
state.send_version = (state.send_version ?? 0) + 1;
|
|
451
|
+
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
390
452
|
saveStreamState(sessionId, state);
|
|
391
453
|
}
|
|
392
454
|
function processAfterTool(hookData) {
|
|
@@ -478,11 +540,13 @@ function processChunk(hookData) {
|
|
|
478
540
|
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
479
541
|
state.send_version = (state.send_version ?? 0) + 1;
|
|
480
542
|
toolCallDetected = true;
|
|
481
|
-
|
|
543
|
+
// Start a new text round after tool completes
|
|
544
|
+
state.current_round = (state.current_round ?? 0) + 1;
|
|
545
|
+
debug(`Tool call detected via msg_count (${savedMsgCount} → ${currentMsgCount}), tool_turns=${state.tool_turns}, round=${state.current_round}`);
|
|
482
546
|
}
|
|
483
547
|
}
|
|
484
548
|
state.msg_count = currentMsgCount;
|
|
485
|
-
// Accumulate text and
|
|
549
|
+
// Accumulate text into both total and per-round tracking
|
|
486
550
|
if (chunkText) {
|
|
487
551
|
if (!state.first_chunk_time)
|
|
488
552
|
state.first_chunk_time = nowISO();
|
|
@@ -490,10 +554,21 @@ function processChunk(hookData) {
|
|
|
490
554
|
state.last_tokens = completionTokens || state.last_tokens;
|
|
491
555
|
if (thoughtsTokens > 0)
|
|
492
556
|
state.thoughts_tokens = thoughtsTokens;
|
|
493
|
-
|
|
494
|
-
|
|
557
|
+
// Track text per round
|
|
558
|
+
const round = state.current_round ?? 0;
|
|
559
|
+
if (!state.text_rounds)
|
|
560
|
+
state.text_rounds = [];
|
|
561
|
+
if (!state.round_start_times)
|
|
562
|
+
state.round_start_times = [];
|
|
563
|
+
while (state.text_rounds.length <= round)
|
|
564
|
+
state.text_rounds.push('');
|
|
565
|
+
while (state.round_start_times.length <= round)
|
|
566
|
+
state.round_start_times.push('');
|
|
567
|
+
state.text_rounds[round] += chunkText;
|
|
568
|
+
if (!state.round_start_times[round])
|
|
569
|
+
state.round_start_times[round] = nowISO();
|
|
495
570
|
saveStreamState(sessionId, state);
|
|
496
|
-
debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}`);
|
|
571
|
+
debug(`Accumulated chunk: +${chunkText.length} chars, total=${state.accumulated_text.length}, round=${round}`);
|
|
497
572
|
}
|
|
498
573
|
// Tool call in response parts
|
|
499
574
|
const isToolTurn = hasToolCall || ['TOOL_CALLS', 'FUNCTION_CALL', 'TOOL_USE'].includes(finishReason);
|
|
@@ -533,7 +608,7 @@ function processChunk(hookData) {
|
|
|
533
608
|
const finalTotal = Number(usage.totalTokenCount ?? 0) || (finalPrompt + finalCompletion);
|
|
534
609
|
const tok = { prompt_tokens: finalPrompt, completion_tokens: finalCompletion, total_tokens: finalTotal };
|
|
535
610
|
const config = loadRespanConfig(path.join(os.homedir(), '.gemini', 'respan.json'));
|
|
536
|
-
const spans = buildSpans(hookData, state.accumulated_text, tok, config, state.first_chunk_time || undefined, state.tool_turns ?? 0, state.tool_details ?? [], state.thoughts_tokens ?? 0);
|
|
611
|
+
const spans = buildSpans(hookData, state.accumulated_text, tok, config, state.first_chunk_time || undefined, state.tool_turns ?? 0, state.tool_details ?? [], state.thoughts_tokens ?? 0, state.text_rounds ?? [], state.round_start_times ?? []);
|
|
537
612
|
// Method b: text + STOP → send immediately
|
|
538
613
|
if (isFinished && chunkText) {
|
|
539
614
|
debug(`Immediate send (text+STOP, tool_turns=${state.tool_turns ?? 0}), ${state.accumulated_text.length} chars`);
|