Class: Mistri::Sinks::Logger

Inherits:
Object
  • Object
show all
Defined in:
lib/mistri/sinks/logger.rb

Overview

Renders the event stream as one log line per meaningful beat, so a log tells each run's whole story: which tools ran with which arguments, how long each took, what every turn cost, and how the run ended. Deltas never log (whole blocks do).

Assigning Mistri.logger turns this on for every run in one line:

Mistri.logger = Rails.logger
Mistri.logger = Mistri::Sinks::Logger.new(logger, color: true)

Payloads (inputs, text, thinking, arguments, results, reports) log in full by default; content: false keeps the same story with metadata only, for logs whose payloads must stay out. level: floors the ordinary lines (warnings and errors keep their own levels).

Sinks the Agent builds through for_session skip origin-tagged events, because every agent logs its own events under its own tag: exactly once, however deep the nesting and whichever process runs it. A sink composed directly (agent.run(input, &sink)) is the only logger its run has, so it renders forwarded child events instead, origin first; framing (run, done) still comes only from the Agent.

Any object responding to info/warn/error works. Logging must never break a run: every entry point rescues, the first failure warns, and the sink goes quiet for the rest of its run. Tool events arrive from executor threads; the only mutable state (the turn counter) moves on loop-thread events, and interleaved lines stay whole as long as the host's logger serializes writes, which stdlib Logger and Rails do.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(logger, session: nil, truncate: 200, color: false, level: :info, content: true, forwarded: :render) ⇒ Logger

truncate bounds every rendered value (nil for full payloads). session is the line tag, verbatim, so concurrent runs stay distinguishable. Options are validated here so a bad configuration fails at assignment time, not silently at run time.

Raises:

  • (ArgumentError)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/mistri/sinks/logger.rb', line 58

def initialize(logger, session: nil, truncate: 200, color: false, level: :info,
               content: true, forwarded: :render)
  raise ArgumentError, "level must be one of #{LEVELS.join(", ")}" unless
    LEVELS.include?(level)
  unless truncate.nil? || (truncate.is_a?(Integer) && truncate.positive?)
    raise ArgumentError, "truncate must be a positive Integer or nil"
  end
  raise ArgumentError, "forwarded must be :render or :skip" unless
    %i[render skip].include?(forwarded)

  @logger = logger
  @truncate = truncate
  @color = color
  @level = level
  @content = content
  @forwarded = forwarded
  @session = session && field(session, limit: 48)
  @turns = 0
  @started = now
  @warned = false
  @tag = paint(@session ? "[mistri #{@session}]" : "[mistri]", :dim)
end

Class Method Details

.attach(id, label: nil) ⇒ Object

The global hookup: builds this run's sink from Mistri.logger, or nil. Contained, so a broken assignment costs the log, never the run.



92
93
94
95
96
97
98
99
100
101
# File 'lib/mistri/sinks/logger.rb', line 92

def self.attach(id, label: nil)
  configured = Mistri.logger
  return nil unless configured

  sink = configured.is_a?(self) ? configured : new(configured)
  sink.for_session(id, label: label)
rescue StandardError => e
  warn "mistri: logging sink construction failed (#{e.class}: #{e.message}); run not logged"
  nil
end

Instance Method Details

#call(event) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mistri/sinks/logger.rb', line 103

def call(event)
  return if @warned

  return if event.origin && (@forwarded == :skip)
  return if SKIPPED[event.type]
  return if QUIET[event.type] && !floor_enabled?

  handler = LINES[event.type]
  prefix = event.origin ? "#{field(event.origin)} " : ""
  # Future event types degrade to a greppable line, never to silence.
  line = handler ? send(handler, event) : "#{event.type} #{body_of(event.content)}".rstrip
  write("#{prefix}#{line.first}", level: line.last) if line.is_a?(Array)
  write("#{prefix}#{line}") if line.is_a?(String)
rescue StandardError => e
  quiet(e)
end

#for_session(id, label: nil) ⇒ Object

A fresh sink for one run: same logger and options, this run's tag (a sub-agent's label, or the session id), its own turn count and clock. Runs framed this way skip forwarded events, because each agent in the tree logs its own.



85
86
87
88
# File 'lib/mistri/sinks/logger.rb', line 85

def for_session(id, label: nil)
  self.class.new(@logger, session: label || id.to_s[0, 8], truncate: @truncate,
                          color: @color, level: @level, content: @content, forwarded: :skip)
end

#run_crashed(error) ⇒ Object



147
148
149
150
151
# File 'lib/mistri/sinks/logger.rb', line 147

def run_crashed(error)
  write("#{paint("crashed", :red)} #{error.class}#{note(error.message)}", level: :error)
rescue StandardError => e
  quiet(e)
end

#run_finished(result) ⇒ Object



137
138
139
140
141
142
143
144
145
# File 'lib/mistri/sinks/logger.rb', line 137

def run_finished(result)
  status, level = status_of(result)
  return if level == :info && !floor_enabled?

  write("#{paint("done", :bold)} #{status} in #{clock(now - @started)}, " \
        "#{count(@turns, "turn")}#{summary(result.usage)}", level: level)
rescue StandardError => e
  quiet(e)
end

#run_started(verb:, model:, tool_count:, input: nil) ⇒ Object

The framing the stream cannot carry: the Agent calls these around a run with the input, the model, and the final Result. Each is total for the same reason call is: a logging failure inside a rescue block must never replace the host's own exception.



126
127
128
129
130
131
132
133
134
135
# File 'lib/mistri/sinks/logger.rb', line 126

def run_started(verb:, model:, tool_count:, input: nil)
  @turns = 0
  @started = now
  return unless floor_enabled?

  ask = input && @content ? %( "#{trim(input)}") : ""
  write("#{paint(verb, :bold)}#{ask} (#{field(model)}, #{count(tool_count, "tool")})")
rescue StandardError => e
  quiet(e)
end

#to_procObject



120
# File 'lib/mistri/sinks/logger.rb', line 120

def to_proc = method(:call).to_proc