#!/usr/bin/perl use strict; use warnings; use JSON::PP; use Time::Local; my ($file, $cutoff_iso) = @ARGV; # cutoff_iso optional: only count entries with timestamp >= this my $cutoff = 0; if ($cutoff_iso) { $cutoff = iso_to_epoch($cutoff_iso); } sub iso_to_epoch { my ($s) = @_; return 0 unless defined $s; if ($s =~ /^\d+$/) { return $s + 0; } # already epoch ms if ($s =~ /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)\.(\d+)Z/) { return timegm($6,$5,$4,$3,$2-1,$1) * 1000 + $7; } return 0; } open(my $fh, "<:encoding(UTF-8)", $file) or die $!; my @msgs; while (my $line = <$fh>) { chomp $line; next unless $line; my $obj = eval { decode_json($line) }; next unless $obj; next unless $obj->{type} eq "message"; my $ts = iso_to_epoch($obj->{message}{timestamp} // $obj->{timestamp} // ''); next if $cutoff && $ts < $cutoff; push @msgs, { obj=>$obj, ts=>$ts }; } my $nAssistant=0; my %toolCounts; my @calls; # each: name,args,issued_ts,id my %callById; my @results; # {id, ts, isError, textlen} my $totalInTok=0; my $totalOutTok=0; my $maxInTok=0; my $firstTs; my $lastTs; for my $m (@msgs) { my $obj = $m->{obj}; my $role = $obj->{message}{role} // '?'; $firstTs = $m->{ts} if !defined($firstTs) || $m->{ts} < $firstTs; $lastTs = $m->{ts} if !defined($lastTs) || $m->{ts} > $lastTs; if ($role eq 'assistant') { $nAssistant++; my $usage = $obj->{message}{usage}; if ($usage) { my $in = $usage->{input} // $usage->{inputTokens} // $usage->{prompt_tokens} // 0; my $out = $usage->{output} // $usage->{outputTokens} // $usage->{completion_tokens} // 0; $totalInTok += $in; $totalOutTok += $out; $maxInTok = $in if $in > $maxInTok; } for my $c (@{$obj->{message}{content}}) { if ($c->{type} eq 'toolCall') { $toolCounts{$c->{name}}++; my $argstr = encode_json($c->{arguments}); $callById{$c->{id}} = { name=>$c->{name}, args=>$argstr, issued=>$m->{ts} }; } } } elsif ($role eq 'toolResult') { my $tcid = $obj->{message}{toolCallId}; my $isErr = $obj->{message}{isError} ? 1 : 0; my $text = ''; for my $c (@{$obj->{message}{content}}) { $text .= ($c->{text}//'') if $c->{type} eq 'text'; } push @results, { id=>$tcid, ts=>$m->{ts}, isError=>$isErr, text=>substr($text,0,200) }; } } # compute durations: result.ts - issued.ts (per call) my @durs; for my $r (@results) { my $c = $callById{$r->{id}}; next unless $c; my $dur = $r->{ts} - $c->{issued}; push @durs, { name=>$c->{name}, args=>$c->{args}, dur=>$dur, isError=>$r->{isError}, err=>$r->{text} }; } @durs = sort { $b->{dur} <=> $a->{dur} } @durs; print "=== $file ===\n"; print "cutoff: $cutoff_iso ($cutoff)\n" if $cutoff_iso; print "assistant turns: $nAssistant\n"; print "tool calls total: ", scalar(keys %callById), "\n"; print "tool calls by type: "; for my $k (sort keys %toolCounts) { print "$k=$toolCounts{$k} "; } print "\n"; print "total input tokens (sum): $totalInTok max single-call input: $maxInTok total output tokens: $totalOutTok\n"; print "span: ", ($lastTs-$firstTs)/1000, "s first=$firstTs last=$lastTs\n"; my $toolTimeSum = 0; $toolTimeSum += $_->{dur} for @durs; print "total tool time (sum all calls): ", $toolTimeSum/1000, "s\n"; print "--- top 10 longest tool calls ---\n"; for my $i (0..9) { last unless $durs[$i]; my $d = $durs[$i]; my $a = $d->{args}; $a =~ s/\s+/ /g; $a = substr($a,0,100); printf("%7.1fs %-8s %s %s\n", $d->{dur}/1000, $d->{name}, $a, ($d->{isError}?"ERROR":"")); } print "--- errors ---\n"; for my $d (@durs) { if ($d->{isError}) { my $a = $d->{args}; $a =~ s/\s+/ /g; $a = substr($a,0,100); my $e = $d->{err}; $e =~ s/\s+/ /g; print "$d->{name} $a :: $e\n"; } }