Class: Agentilda::Transcript
- Inherits:
-
Object
- Object
- Agentilda::Transcript
- Defined in:
- lib/agentilda/transcript.rb
Overview
What an agent is doing, while it is still doing it.
claude -p prints nothing until it exits, so a fifteen-minute agent used to
sit behind a spinner that said only its name. Asked for --output-format stream-json it emits one JSON object per line as it works, and this turns
that stream into the short phrase a spinner line has room for: "editing
spec.md", "reading plan.md", whatever the agent last said about itself.
Three things make this less trivial than parsing JSON.
Chunks arrive from readpartial, so a line is split across two of them as
often as not. #push buffers and only ever parses whole lines.
Not every line is JSON. claude reports its own failures as prose on
stdout, and the 401 that cost a whole run three minutes an agent was one of
those. Those lines are kept in #plain so a failure can still be explained
by what was actually printed.
The reader runs on its own thread, so #activity is written from one thread and read from another. It is a single assignment of an immutable string, which is why there is no lock here.
Every line is also kept verbatim in an optional trace file, written before anything is made of it, so a run that failed can be read back afterwards even where this parser made nothing of an event it had never seen. See Executor::TRACE_DIR for where those land and how to read one.
Defined Under Namespace
Classes: Progress
Constant Summary collapse
- DELTA_NOISE =
The one kind of line the trace does not keep.
--include-partial-messagesis what makes #meter possible, and it also emits one event per handful of generated characters. Those are the bulk of the stream and the only part of it nothing reads back: a fifteen-minute agent writes thousands of them, and they would bury the events a failed run is actually read for. Matched as a substring rather than parsed because the trace is written before a line is understood, which is what makes it complete when the process dies mid-invocation. '"content_block_delta"'- LIMIT =
Longest phrase a spinner line can carry without wrapping into the next.
56- VERBS =
How each tool reads as something being done, rather than as a tool name. A spinner says what is happening; "Edit" is a noun and says nothing.
{ "Read" => "reading", "Write" => "writing", "Edit" => "editing", "MultiEdit" => "editing", "NotebookEdit" => "editing", "Bash" => "running", "Grep" => "searching for", "Glob" => "looking for", "Task" => "delegating", "TodoWrite" => "planning", "WebFetch" => "fetching", "WebSearch" => "searching the web for" }.freeze
- SUBJECTS =
The input key worth naming, per tool, in the order we would rather say it.
descriptionoutrankscommanddeliberately: Claude's Bash tool carries a written summary of why it is running something, and "Read NOTES.md" is a better spinner line than sixty characters of absolute path. %w[file_path pattern description command query url].freeze
Instance Attribute Summary collapse
-
#activity ⇒ String?
readonly
The last thing the agent was seen doing.
-
#error ⇒ String?
readonly
What the agent said went wrong.
-
#pid ⇒ Integer?
The
claudeprocess this transcript is reading, set by the Executor once it has found the child, so the spinner line can name a pid someone can actuallypswhile the agent runs. -
#plain ⇒ Array<String>
readonly
Lines that were not JSON, in order.
-
#result ⇒ String?
readonly
The final text, from the
resultevent. -
#spawned ⇒ Integer
readonly
Sub-agents this invocation started.
-
#tools ⇒ Integer
readonly
How many tool calls have gone past, which is the one honest measure of how much work an agent that says "done" actually did.
-
#trace_path ⇒ String?
readonly
The file the raw stream is being kept in.
Instance Method Summary collapse
-
#delegated ⇒ Integer
Sub-agent totals arrive as one number with no split between what went up and what came back, so they are counted as #up.
-
#down ⇒ Integer
Tokens the model generated, thinking included.
-
#failed? ⇒ Boolean
Whether the stream reported its own failure.
-
#finish ⇒ void
Whatever is left when the process exits, which may be a last line with no newline after it.
-
#initialize(trace: nil) {|progress| ... } ⇒ Transcript
constructor
A new instance of Transcript.
-
#progress ⇒ Agentilda::Transcript::Progress
A snapshot, safe to keep.
-
#push(chunk) ⇒ void
Feed one chunk of stdout.
-
#up ⇒ Integer
Everything sent to the model, which on any turn after the first is mostly cache reads: a typical agent turn is two fresh input tokens against a quarter of a million read back from cache.
Constructor Details
#initialize(trace: nil) {|progress| ... } ⇒ Transcript
Returns a new instance of Transcript.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/agentilda/transcript.rb', line 90 def initialize(trace: nil, &on_progress) @on_progress = on_progress @buffer = +"" @plain = [] @activity = nil @result = nil @error = nil @trace_path = trace @trace = trace && File.open(trace, "a") @tools = 0 @main = {up: 0, down: 0} @streamed = {} @tasks = {} @spawned = 0 end |
Instance Attribute Details
#activity ⇒ String? (readonly)
Returns the last thing the agent was seen doing.
149 150 151 |
# File 'lib/agentilda/transcript.rb', line 149 def activity @activity end |
#error ⇒ String? (readonly)
Returns what the agent said went wrong.
161 162 163 |
# File 'lib/agentilda/transcript.rb', line 161 def error @error end |
#pid ⇒ Integer?
The claude process this transcript is reading, set by the Executor
once it has found the child, so the spinner line can name a pid someone
can actually ps while the agent runs.
125 126 127 |
# File 'lib/agentilda/transcript.rb', line 125 def pid @pid end |
#plain ⇒ Array<String> (readonly)
Returns lines that were not JSON, in order.
155 156 157 |
# File 'lib/agentilda/transcript.rb', line 155 def plain @plain end |
#result ⇒ String? (readonly)
Returns the final text, from the result event.
152 153 154 |
# File 'lib/agentilda/transcript.rb', line 152 def result @result end |
#spawned ⇒ Integer (readonly)
Returns sub-agents this invocation started.
118 119 120 |
# File 'lib/agentilda/transcript.rb', line 118 def spawned @spawned end |
#tools ⇒ Integer (readonly)
Returns how many tool calls have gone past, which is the one honest measure of how much work an agent that says "done" actually did.
143 144 145 |
# File 'lib/agentilda/transcript.rb', line 143 def tools @tools end |
#trace_path ⇒ String? (readonly)
Returns the file the raw stream is being kept in.
146 147 148 |
# File 'lib/agentilda/transcript.rb', line 146 def trace_path @trace_path end |
Instance Method Details
#delegated ⇒ Integer
Sub-agent totals arrive as one number with no split between what went up and what came back, so they are counted as #up. They are held here as well, unmixed, because a report that says "of which N could not be split" is honest and one that quietly rounds is not.
133 134 135 136 |
# File 'lib/agentilda/transcript.rb', line 133 def delegated @tasks.reject { |_, task| @streamed.key?(task[:tool_use_id]) } .sum { |_, task| task[:total] } end |
#down ⇒ Integer
Returns tokens the model generated, thinking included.
115 |
# File 'lib/agentilda/transcript.rb', line 115 def down = @main[:down] + streamed[:down] |
#failed? ⇒ Boolean
Returns whether the stream reported its own failure.
158 |
# File 'lib/agentilda/transcript.rb', line 158 def failed? = !@error.nil? |
#finish ⇒ void
This method returns an undefined value.
Whatever is left when the process exits, which may be a last line with no newline after it.
182 183 184 185 186 |
# File 'lib/agentilda/transcript.rb', line 182 def finish consume(@buffer.slice!(0..-1).to_s) @trace&.close @trace = nil end |
#progress ⇒ Agentilda::Transcript::Progress
Returns a snapshot, safe to keep.
139 |
# File 'lib/agentilda/transcript.rb', line 139 def progress = Progress.new(activity: @activity, up:, down:, subagents: @spawned, pid: @pid) |
#push(chunk) ⇒ void
This method returns an undefined value.
Feed one chunk of stdout. Safe to call with a partial line, which is the normal case.
168 169 170 171 172 173 174 175 176 |
# File 'lib/agentilda/transcript.rb', line 168 def push(chunk) return if chunk.nil? @buffer << chunk while (index = @buffer.index("\n")) line = @buffer.slice!(0..index).chomp consume(line) end end |
#up ⇒ Integer
Everything sent to the model, which on any turn after the first is mostly cache reads: a typical agent turn is two fresh input tokens against a quarter of a million read back from cache. Counting only the fresh ones would report a fifteen-minute agent as having said nothing.
112 |
# File 'lib/agentilda/transcript.rb', line 112 def up = @main[:up] + streamed[:up] + delegated |