@respan/cli 0.6.8 → 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 +107 -48
- package/dist/hooks/gemini-cli.js +128 -57
- package/oclif.manifest.json +444 -444
- 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
|
}
|
|
@@ -748,7 +799,8 @@ function processChunk(hookData) {
|
|
|
748
799
|
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
749
800
|
state.send_version = (state.send_version ?? 0) + 1;
|
|
750
801
|
toolCallDetected = true;
|
|
751
|
-
|
|
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}`);
|
|
752
804
|
}
|
|
753
805
|
}
|
|
754
806
|
state.msg_count = currentMsgCount;
|
|
@@ -757,10 +809,15 @@ function processChunk(hookData) {
|
|
|
757
809
|
state.accumulated_text += chunkText;
|
|
758
810
|
state.last_tokens = completionTokens || state.last_tokens;
|
|
759
811
|
if (thoughtsTokens > 0) state.thoughts_tokens = thoughtsTokens;
|
|
760
|
-
|
|
761
|
-
|
|
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();
|
|
762
819
|
saveStreamState(sessionId, state);
|
|
763
|
-
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}`);
|
|
764
821
|
}
|
|
765
822
|
const isToolTurn = hasToolCall || ["TOOL_CALLS", "FUNCTION_CALL", "TOOL_USE"].includes(finishReason);
|
|
766
823
|
if (isToolTurn) {
|
|
@@ -800,7 +857,9 @@ function processChunk(hookData) {
|
|
|
800
857
|
state.first_chunk_time || void 0,
|
|
801
858
|
state.tool_turns ?? 0,
|
|
802
859
|
state.tool_details ?? [],
|
|
803
|
-
state.thoughts_tokens ?? 0
|
|
860
|
+
state.thoughts_tokens ?? 0,
|
|
861
|
+
state.text_rounds ?? [],
|
|
862
|
+
state.round_start_times ?? []
|
|
804
863
|
);
|
|
805
864
|
if (isFinished && chunkText) {
|
|
806
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
|
}
|
|
@@ -482,11 +540,13 @@ function processChunk(hookData) {
|
|
|
482
540
|
state.tool_turns = (state.tool_turns ?? 0) + 1;
|
|
483
541
|
state.send_version = (state.send_version ?? 0) + 1;
|
|
484
542
|
toolCallDetected = true;
|
|
485
|
-
|
|
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}`);
|
|
486
546
|
}
|
|
487
547
|
}
|
|
488
548
|
state.msg_count = currentMsgCount;
|
|
489
|
-
// Accumulate text and
|
|
549
|
+
// Accumulate text into both total and per-round tracking
|
|
490
550
|
if (chunkText) {
|
|
491
551
|
if (!state.first_chunk_time)
|
|
492
552
|
state.first_chunk_time = nowISO();
|
|
@@ -494,10 +554,21 @@ function processChunk(hookData) {
|
|
|
494
554
|
state.last_tokens = completionTokens || state.last_tokens;
|
|
495
555
|
if (thoughtsTokens > 0)
|
|
496
556
|
state.thoughts_tokens = thoughtsTokens;
|
|
497
|
-
|
|
498
|
-
|
|
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();
|
|
499
570
|
saveStreamState(sessionId, state);
|
|
500
|
-
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}`);
|
|
501
572
|
}
|
|
502
573
|
// Tool call in response parts
|
|
503
574
|
const isToolTurn = hasToolCall || ['TOOL_CALLS', 'FUNCTION_CALL', 'TOOL_USE'].includes(finishReason);
|
|
@@ -537,7 +608,7 @@ function processChunk(hookData) {
|
|
|
537
608
|
const finalTotal = Number(usage.totalTokenCount ?? 0) || (finalPrompt + finalCompletion);
|
|
538
609
|
const tok = { prompt_tokens: finalPrompt, completion_tokens: finalCompletion, total_tokens: finalTotal };
|
|
539
610
|
const config = loadRespanConfig(path.join(os.homedir(), '.gemini', 'respan.json'));
|
|
540
|
-
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 ?? []);
|
|
541
612
|
// Method b: text + STOP → send immediately
|
|
542
613
|
if (isFinished && chunkText) {
|
|
543
614
|
debug(`Immediate send (text+STOP, tool_turns=${state.tool_turns ?? 0}), ${state.accumulated_text.length} chars`);
|