@respan/cli 0.7.0 → 0.7.1

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.
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  var fs2 = __toESM(require("node:fs"), 1);
27
27
  var os2 = __toESM(require("node:os"), 1);
28
28
  var path2 = __toESM(require("node:path"), 1);
29
+ var import_node_child_process = require("node:child_process");
29
30
 
30
31
  // src/hooks/shared.ts
31
32
  var fs = __toESM(require("node:fs"), 1);
@@ -699,50 +700,18 @@ function findLatestSessionFile() {
699
700
  return null;
700
701
  }
701
702
  }
702
- async function main() {
703
+ async function mainWorker() {
703
704
  const scriptStart = Date.now();
704
- debug("Codex hook started");
705
- if (process.argv.length < 3) {
706
- debug("No argument provided (expected JSON payload in argv[2])");
707
- process.exit(0);
708
- }
709
- let payload;
710
- try {
711
- payload = JSON.parse(process.argv[2]);
712
- } catch (e) {
713
- debug(`Invalid JSON in argv[2]: ${e}`);
714
- process.exit(0);
715
- }
716
- const eventType = String(payload.type ?? "");
717
- if (eventType !== "agent-turn-complete") {
718
- debug(`Ignoring event type: ${eventType}`);
719
- process.exit(0);
720
- }
721
- let sessionId = String(payload["thread-id"] ?? "");
722
- if (!sessionId) {
723
- debug("No thread-id in notify payload");
724
- process.exit(0);
725
- }
726
- debug(`Processing notify: type=${eventType}, session=${sessionId}`);
705
+ debug("Worker started");
706
+ const sessionId = process.env._RESPAN_CODEX_SESSION;
707
+ const sessionFile = process.env._RESPAN_CODEX_FILE;
708
+ const cwd = process.env._RESPAN_CODEX_CWD ?? "";
727
709
  const creds = resolveCredentials();
728
710
  if (!creds) {
729
- log("ERROR", "No API key found. Run: respan auth login");
730
- process.exit(0);
731
- }
732
- let sessionFile = findSessionFile(sessionId);
733
- if (!sessionFile) {
734
- const latest = findLatestSessionFile();
735
- if (latest) {
736
- sessionId = latest.sessionId;
737
- sessionFile = latest.sessionFile;
738
- } else {
739
- debug("No session file found");
740
- process.exit(0);
741
- }
711
+ log("ERROR", "No API key");
712
+ return;
742
713
  }
743
- const cwd = String(payload.cwd ?? "");
744
714
  const config = cwd ? loadRespanConfig(path2.join(cwd, ".codex", "respan.json")) : null;
745
- if (config) debug(`Loaded respan.json config from ${cwd}`);
746
715
  const maxAttempts = 3;
747
716
  let turns = 0;
748
717
  try {
@@ -774,20 +743,73 @@ async function main() {
774
743
  }
775
744
  if (turns > 0) break;
776
745
  if (attempt < maxAttempts - 1) {
777
- const delay = 500 * (attempt + 1);
778
- debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
779
- await new Promise((r) => setTimeout(r, delay));
746
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
780
747
  }
781
748
  }
782
749
  const duration = (Date.now() - scriptStart) / 1e3;
783
750
  log("INFO", `Processed ${turns} turns in ${duration.toFixed(1)}s`);
784
- if (duration > 180) log("WARN", `Hook took ${duration.toFixed(1)}s (>3min)`);
785
751
  } catch (e) {
786
752
  log("ERROR", `Failed to process session: ${e}`);
787
- if (DEBUG_MODE) debug(String(e.stack ?? e));
788
753
  }
789
754
  }
790
- main().catch((e) => {
791
- log("ERROR", `Hook crashed: ${e}`);
792
- process.exit(1);
793
- });
755
+ function main() {
756
+ if (process.env._RESPAN_CODEX_WORKER === "1") {
757
+ mainWorker().catch((e) => log("ERROR", `Worker crashed: ${e}`));
758
+ return;
759
+ }
760
+ debug("Codex hook started");
761
+ if (process.argv.length < 3) {
762
+ debug("No argument provided");
763
+ process.exit(0);
764
+ }
765
+ let payload;
766
+ try {
767
+ payload = JSON.parse(process.argv[2]);
768
+ } catch (e) {
769
+ debug(`Invalid JSON in argv[2]: ${e}`);
770
+ process.exit(0);
771
+ }
772
+ const eventType = String(payload.type ?? "");
773
+ if (eventType !== "agent-turn-complete") {
774
+ debug(`Ignoring event type: ${eventType}`);
775
+ process.exit(0);
776
+ }
777
+ let sessionId = String(payload["thread-id"] ?? "");
778
+ if (!sessionId) {
779
+ debug("No thread-id in notify payload");
780
+ process.exit(0);
781
+ }
782
+ let sessionFile = findSessionFile(sessionId);
783
+ if (!sessionFile) {
784
+ const latest = findLatestSessionFile();
785
+ if (latest) {
786
+ sessionId = latest.sessionId;
787
+ sessionFile = latest.sessionFile;
788
+ } else {
789
+ debug("No session file found");
790
+ process.exit(0);
791
+ }
792
+ }
793
+ const cwd = String(payload.cwd ?? "");
794
+ debug(`Forking worker for session: ${sessionId}`);
795
+ try {
796
+ const scriptPath = __filename || process.argv[1];
797
+ const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
798
+ env: {
799
+ ...process.env,
800
+ _RESPAN_CODEX_WORKER: "1",
801
+ _RESPAN_CODEX_SESSION: sessionId,
802
+ _RESPAN_CODEX_FILE: sessionFile,
803
+ _RESPAN_CODEX_CWD: cwd
804
+ },
805
+ stdio: "ignore",
806
+ detached: true
807
+ });
808
+ child.unref();
809
+ debug("Worker launched");
810
+ } catch (e) {
811
+ log("ERROR", `Failed to fork worker: ${e}`);
812
+ }
813
+ process.exit(0);
814
+ }
815
+ main();
@@ -16,6 +16,7 @@
16
16
  import * as fs from 'node:fs';
17
17
  import * as os from 'node:os';
18
18
  import * as path from 'node:path';
19
+ import { execFile } from 'node:child_process';
19
20
  import { initLogging, log, debug, resolveCredentials, loadRespanConfig, loadState, saveState, acquireLock, sendSpans, addDefaultsToAll, resolveSpanFields, buildMetadata, nowISO, latencySeconds, truncate, } from './shared.js';
20
21
  // ── Config ────────────────────────────────────────────────────────
21
22
  const STATE_DIR = path.join(os.homedir(), '.codex', 'state');
@@ -363,57 +364,18 @@ function findLatestSessionFile() {
363
364
  }
364
365
  }
365
366
  // ── Main ──────────────────────────────────────────────────────────
366
- async function main() {
367
+ async function mainWorker() {
367
368
  const scriptStart = Date.now();
368
- debug('Codex hook started');
369
- // Parse notify payload from argv[2] (argv[0]=node, argv[1]=script)
370
- if (process.argv.length < 3) {
371
- debug('No argument provided (expected JSON payload in argv[2])');
372
- process.exit(0);
373
- }
374
- let payload;
375
- try {
376
- payload = JSON.parse(process.argv[2]);
377
- }
378
- catch (e) {
379
- debug(`Invalid JSON in argv[2]: ${e}`);
380
- process.exit(0);
381
- }
382
- const eventType = String(payload.type ?? '');
383
- if (eventType !== 'agent-turn-complete') {
384
- debug(`Ignoring event type: ${eventType}`);
385
- process.exit(0);
386
- }
387
- let sessionId = String(payload['thread-id'] ?? '');
388
- if (!sessionId) {
389
- debug('No thread-id in notify payload');
390
- process.exit(0);
391
- }
392
- debug(`Processing notify: type=${eventType}, session=${sessionId}`);
369
+ debug('Worker started');
370
+ const sessionId = process.env._RESPAN_CODEX_SESSION;
371
+ const sessionFile = process.env._RESPAN_CODEX_FILE;
372
+ const cwd = process.env._RESPAN_CODEX_CWD ?? '';
393
373
  const creds = resolveCredentials();
394
374
  if (!creds) {
395
- log('ERROR', 'No API key found. Run: respan auth login');
396
- process.exit(0);
397
- }
398
- // Find session file
399
- let sessionFile = findSessionFile(sessionId);
400
- if (!sessionFile) {
401
- const latest = findLatestSessionFile();
402
- if (latest) {
403
- sessionId = latest.sessionId;
404
- sessionFile = latest.sessionFile;
405
- }
406
- else {
407
- debug('No session file found');
408
- process.exit(0);
409
- }
375
+ log('ERROR', 'No API key');
376
+ return;
410
377
  }
411
- // Load config
412
- const cwd = String(payload.cwd ?? '');
413
378
  const config = cwd ? loadRespanConfig(path.join(cwd, '.codex', 'respan.json')) : null;
414
- if (config)
415
- debug(`Loaded respan.json config from ${cwd}`);
416
- // Process with retry
417
379
  const maxAttempts = 3;
418
380
  let turns = 0;
419
381
  try {
@@ -447,23 +409,80 @@ async function main() {
447
409
  if (turns > 0)
448
410
  break;
449
411
  if (attempt < maxAttempts - 1) {
450
- const delay = 500 * (attempt + 1);
451
- debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
452
- await new Promise((r) => setTimeout(r, delay));
412
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
453
413
  }
454
414
  }
455
415
  const duration = (Date.now() - scriptStart) / 1000;
456
416
  log('INFO', `Processed ${turns} turns in ${duration.toFixed(1)}s`);
457
- if (duration > 180)
458
- log('WARN', `Hook took ${duration.toFixed(1)}s (>3min)`);
459
417
  }
460
418
  catch (e) {
461
419
  log('ERROR', `Failed to process session: ${e}`);
462
- if (DEBUG_MODE)
463
- debug(String(e.stack ?? e));
464
420
  }
465
421
  }
466
- main().catch((e) => {
467
- log('ERROR', `Hook crashed: ${e}`);
468
- process.exit(1);
469
- });
422
+ function main() {
423
+ // Worker mode: re-invoked as detached subprocess
424
+ if (process.env._RESPAN_CODEX_WORKER === '1') {
425
+ mainWorker().catch((e) => log('ERROR', `Worker crashed: ${e}`));
426
+ return;
427
+ }
428
+ debug('Codex hook started');
429
+ if (process.argv.length < 3) {
430
+ debug('No argument provided');
431
+ process.exit(0);
432
+ }
433
+ let payload;
434
+ try {
435
+ payload = JSON.parse(process.argv[2]);
436
+ }
437
+ catch (e) {
438
+ debug(`Invalid JSON in argv[2]: ${e}`);
439
+ process.exit(0);
440
+ }
441
+ const eventType = String(payload.type ?? '');
442
+ if (eventType !== 'agent-turn-complete') {
443
+ debug(`Ignoring event type: ${eventType}`);
444
+ process.exit(0);
445
+ }
446
+ let sessionId = String(payload['thread-id'] ?? '');
447
+ if (!sessionId) {
448
+ debug('No thread-id in notify payload');
449
+ process.exit(0);
450
+ }
451
+ // Find session file
452
+ let sessionFile = findSessionFile(sessionId);
453
+ if (!sessionFile) {
454
+ const latest = findLatestSessionFile();
455
+ if (latest) {
456
+ sessionId = latest.sessionId;
457
+ sessionFile = latest.sessionFile;
458
+ }
459
+ else {
460
+ debug('No session file found');
461
+ process.exit(0);
462
+ }
463
+ }
464
+ // Fork self as detached worker so Codex CLI doesn't block
465
+ const cwd = String(payload.cwd ?? '');
466
+ debug(`Forking worker for session: ${sessionId}`);
467
+ try {
468
+ const scriptPath = __filename || process.argv[1];
469
+ const child = execFile('node', [scriptPath], {
470
+ env: {
471
+ ...process.env,
472
+ _RESPAN_CODEX_WORKER: '1',
473
+ _RESPAN_CODEX_SESSION: sessionId,
474
+ _RESPAN_CODEX_FILE: sessionFile,
475
+ _RESPAN_CODEX_CWD: cwd,
476
+ },
477
+ stdio: 'ignore',
478
+ detached: true,
479
+ });
480
+ child.unref();
481
+ debug('Worker launched');
482
+ }
483
+ catch (e) {
484
+ log('ERROR', `Failed to fork worker: ${e}`);
485
+ }
486
+ process.exit(0);
487
+ }
488
+ main();
@@ -438,6 +438,34 @@ function detectModel(hookData) {
438
438
  const llmReq = hookData.llm_request ?? {};
439
439
  return String(llmReq.model ?? "") || "gemini-cli";
440
440
  }
441
+ function buildToolSpan(detail, idx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime) {
442
+ const toolName = detail?.name ?? "";
443
+ const toolArgs = detail?.args ?? detail?.input ?? {};
444
+ const toolOutput = detail?.output ?? "";
445
+ const displayName = toolName ? toolDisplayName(toolName) : `Call ${idx + 1}`;
446
+ const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
447
+ const toolMeta = {};
448
+ if (toolName) toolMeta.tool_name = toolName;
449
+ if (detail?.error) toolMeta.error = detail.error;
450
+ const toolStart = detail?.start_time ?? beginTime;
451
+ const toolEnd = detail?.end_time ?? endTime;
452
+ const toolLat = latencySeconds(toolStart, toolEnd);
453
+ return {
454
+ trace_unique_id: traceUniqueId,
455
+ span_unique_id: `gcli_${safeId}_${turnTs}_tool_${idx + 1}`,
456
+ span_parent_id: rootSpanId,
457
+ span_name: `Tool: ${displayName}`,
458
+ span_workflow_name: workflowName,
459
+ span_path: toolName ? `tool_${toolName}` : "tool_call",
460
+ provider_id: "",
461
+ metadata: toolMeta,
462
+ input: toolInputStr,
463
+ output: truncate(toolOutput, MAX_CHARS),
464
+ timestamp: toolEnd,
465
+ start_time: toolStart,
466
+ ...toolLat !== void 0 ? { latency: toolLat } : {}
467
+ };
468
+ }
441
469
  function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens, textRounds, roundStartTimes) {
442
470
  const spans = [];
443
471
  const sessionId = String(hookData.session_id ?? "");
@@ -519,33 +547,7 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
519
547
  }
520
548
  if (r < rounds.length - 1) {
521
549
  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
- });
550
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
549
551
  toolIdx++;
550
552
  const nextDetail = toolDetails[toolIdx];
551
553
  if (nextDetail && roundStarts[r + 1] && nextDetail.start_time && nextDetail.start_time > roundStarts[r + 1]) break;
@@ -553,33 +555,7 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
553
555
  }
554
556
  }
555
557
  while (toolIdx < toolDetails.length) {
556
- const detail = toolDetails[toolIdx];
557
- const toolName = detail?.name ?? "";
558
- const toolArgs = detail?.args ?? detail?.input ?? {};
559
- const toolOutput = detail?.output ?? "";
560
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
561
- const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
562
- const toolMeta = {};
563
- if (toolName) toolMeta.tool_name = toolName;
564
- if (detail?.error) toolMeta.error = detail.error;
565
- const toolStart = detail?.start_time ?? beginTime;
566
- const toolEnd = detail?.end_time ?? endTime;
567
- const toolLat = latencySeconds(toolStart, toolEnd);
568
- spans.push({
569
- trace_unique_id: traceUniqueId,
570
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
571
- span_parent_id: rootSpanId,
572
- span_name: `Tool: ${displayName}`,
573
- span_workflow_name: workflowName,
574
- span_path: toolName ? `tool_${toolName}` : "tool_call",
575
- provider_id: "",
576
- metadata: toolMeta,
577
- input: toolInputStr,
578
- output: truncate(toolOutput, MAX_CHARS),
579
- timestamp: toolEnd,
580
- start_time: toolStart,
581
- ...toolLat !== void 0 ? { latency: toolLat } : {}
582
- });
558
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
583
559
  toolIdx++;
584
560
  }
585
561
  if (thoughtsTokens > 0) {
@@ -142,6 +142,36 @@ function detectModel(hookData) {
142
142
  return String(llmReq.model ?? '') || 'gemini-cli';
143
143
  }
144
144
  // ── Span construction ─────────────────────────────────────────────
145
+ function buildToolSpan(detail, idx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime) {
146
+ const toolName = detail?.name ?? '';
147
+ const toolArgs = detail?.args ?? detail?.input ?? {};
148
+ const toolOutput = detail?.output ?? '';
149
+ const displayName = toolName ? toolDisplayName(toolName) : `Call ${idx + 1}`;
150
+ const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
151
+ const toolMeta = {};
152
+ if (toolName)
153
+ toolMeta.tool_name = toolName;
154
+ if (detail?.error)
155
+ toolMeta.error = detail.error;
156
+ const toolStart = detail?.start_time ?? beginTime;
157
+ const toolEnd = detail?.end_time ?? endTime;
158
+ const toolLat = latencySeconds(toolStart, toolEnd);
159
+ return {
160
+ trace_unique_id: traceUniqueId,
161
+ span_unique_id: `gcli_${safeId}_${turnTs}_tool_${idx + 1}`,
162
+ span_parent_id: rootSpanId,
163
+ span_name: `Tool: ${displayName}`,
164
+ span_workflow_name: workflowName,
165
+ span_path: toolName ? `tool_${toolName}` : 'tool_call',
166
+ provider_id: '',
167
+ metadata: toolMeta,
168
+ input: toolInputStr,
169
+ output: truncate(toolOutput, MAX_CHARS),
170
+ timestamp: toolEnd,
171
+ start_time: toolStart,
172
+ ...(toolLat !== undefined ? { latency: toolLat } : {}),
173
+ };
174
+ }
145
175
  function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens, textRounds, roundStartTimes) {
146
176
  const spans = [];
147
177
  const sessionId = String(hookData.session_id ?? '');
@@ -233,76 +263,18 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
233
263
  }
234
264
  // Tool spans that come after this round (before next round)
235
265
  if (r < rounds.length - 1) {
236
- // Emit all tools between this round and the next
237
266
  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
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
267
268
  toolIdx++;
268
- // If next tool starts after next round's start time, break — it belongs to a later gap
269
269
  const nextDetail = toolDetails[toolIdx];
270
270
  if (nextDetail && roundStarts[r + 1] && nextDetail.start_time && nextDetail.start_time > roundStarts[r + 1])
271
271
  break;
272
272
  }
273
273
  }
274
274
  }
275
- // Any remaining tools not yet emitted (e.g. only one round but tools exist)
275
+ // Any remaining tools not yet emitted
276
276
  while (toolIdx < toolDetails.length) {
277
- const detail = toolDetails[toolIdx];
278
- const toolName = detail?.name ?? '';
279
- const toolArgs = detail?.args ?? detail?.input ?? {};
280
- const toolOutput = detail?.output ?? '';
281
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
282
- const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : '';
283
- const toolMeta = {};
284
- if (toolName)
285
- toolMeta.tool_name = toolName;
286
- if (detail?.error)
287
- toolMeta.error = detail.error;
288
- const toolStart = detail?.start_time ?? beginTime;
289
- const toolEnd = detail?.end_time ?? endTime;
290
- const toolLat = latencySeconds(toolStart, toolEnd);
291
- spans.push({
292
- trace_unique_id: traceUniqueId,
293
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
294
- span_parent_id: rootSpanId,
295
- span_name: `Tool: ${displayName}`,
296
- span_workflow_name: workflowName,
297
- span_path: toolName ? `tool_${toolName}` : 'tool_call',
298
- provider_id: '',
299
- metadata: toolMeta,
300
- input: toolInputStr,
301
- output: truncate(toolOutput, MAX_CHARS),
302
- timestamp: toolEnd,
303
- start_time: toolStart,
304
- ...(toolLat !== undefined ? { latency: toolLat } : {}),
305
- });
277
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
306
278
  toolIdx++;
307
279
  }
308
280
  // Reasoning span