Module: LLMExperiment::Transcript::Claude
- Defined in:
- lib/llm_experiment/transcript/claude.rb
Overview
Claude Code emits stream-json: one system/init event, one assistant event per model request, and a final result event carrying the totals.
Usage is reported per model request, so the context the very first request carried - the auto-attachment signature - is directly observable here.
Constant Summary collapse
- EDIT_TOOLS =
%w[Edit Write NotebookEdit MultiEdit].freeze
Class Method Summary collapse
-
.paths_in_tool_use(use) ⇒ Object
Which file a tool call touched, when it is knowable.
- .summarize(events, impl_files:, test_file: nil) ⇒ Object
- .tool_uses_in(event) ⇒ Object
- .usage_of(event) ⇒ Object
Class Method Details
.paths_in_tool_use(use) ⇒ Object
Which file a tool call touched, when it is knowable. Reads, edits and writes name a path directly; a shell command has to be scanned for one.
94 95 96 97 98 99 100 101 |
# File 'lib/llm_experiment/transcript/claude.rb', line 94 def paths_in_tool_use(use) input = use["input"] || {} direct = [input["file_path"], input["path"], input["notebook_path"]].compact return direct unless direct.empty? text = [input["command"], input["pattern"], input["prompt"]].compact.join(" ") text.scan(%r{[\w./-]+\.(?:rb|erb|yml|yaml)}).uniq end |
.summarize(events, impl_files:, test_file: nil) ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/llm_experiment/transcript/claude.rb', line 17 def summarize(events, impl_files:, test_file: nil) init = events.find { |e| e["subtype"] == "init" } result = events.reverse.find { |e| e["type"] == "result" } turns = [] tool_calls = [] events.each do |event| next unless event["type"] == "assistant" u = usage_of(event) || {} turns << { "input_tokens" => u["input_tokens"].to_i, "output_tokens" => u["output_tokens"].to_i, "cache_creation_input_tokens" => u["cache_creation_input_tokens"].to_i, "cache_read_input_tokens" => u["cache_read_input_tokens"].to_i, "context_tokens" => u["input_tokens"].to_i + u["cache_creation_input_tokens"].to_i + u["cache_read_input_tokens"].to_i, "tool_calls" => tool_uses_in(event).map { |t| t["name"] } } tool_uses_in(event).each do |use| tool_calls << { "name" => use["name"], "paths" => paths_in_tool_use(use), "blob" => (use["input"] || {}).to_json } end end first_defect_read = tool_calls.index { |t| Transcript.references_impl?(t["blob"], impl_files) } first_edit = tool_calls.index { |t| EDIT_TOOLS.include?(t["name"]) } usage = result&.dig("usage") || {} { "agent" => "claude", "model_reported" => init&.dig("model"), "cli_version" => init&.dig("claude_code_version"), # Hermeticity: a clean container should report none of these. If any is # non-empty the trial ran with help it was not supposed to have. "mcp_servers" => init&.dig("mcp_servers") || [], "memory_paths" => init&.dig("memory_paths") || {}, "tools_available" => (init&.dig("tools") || []).size, "num_turns" => result&.dig("num_turns"), "duration_ms" => result&.dig("duration_ms"), "duration_api_ms" => result&.dig("duration_api_ms"), "total_cost_usd" => result&.dig("total_cost_usd"), "stop_reason" => result&.dig("stop_reason"), "is_error" => result&.dig("is_error"), "input_tokens" => usage["input_tokens"], "output_tokens" => usage["output_tokens"], "cache_creation_input_tokens" => usage["cache_creation_input_tokens"], "cache_read_input_tokens" => usage["cache_read_input_tokens"], "thinking_tokens" => usage.dig("output_tokens_details", "thinking_tokens"), "total_tool_calls" => tool_calls.size, "tool_call_names" => tool_calls.map { |t| t["name"] }, "tool_calls_to_first_defect_read" => first_defect_read, "tool_calls_to_first_edit" => first_edit, "read_before_edit" => first_defect_read && first_edit ? first_defect_read < first_edit : nil, # The auto-attachment signature: how much context the very first request # carried, before any tool could have fetched anything. "context_before_first_tool_call" => turns.first&.dig("context_tokens"), "turns" => turns, "test_file" => test_file } end |
.tool_uses_in(event) ⇒ Object
85 86 87 88 89 90 |
# File 'lib/llm_experiment/transcript/claude.rb', line 85 def tool_uses_in(event) content = event.dig("message", "content") return [] unless content.is_a?(Array) content.select { |c| c["type"] == "tool_use" } end |
.usage_of(event) ⇒ Object
81 82 83 |
# File 'lib/llm_experiment/transcript/claude.rb', line 81 def usage_of(event) event.dig("message", "usage") || event["usage"] end |