// ============================================================
// Bounded execution trace
//
// Printing every tick is useful during development but is too noisy for a
// live game server or agent service. TraceBuffer keeps a bounded in-memory
// window, records the blackboard size seen by each node, and exports stable
// rows for diagnostics. The ring behavior prevents a long-running process
// from retaining an unbounded history.
// ============================================================
///|
/// One observed node execution in a trace window.
pub struct TraceEntry {
sequence : Int
label : String
status : Status
blackboard_size : Int
}
///|
/// Monotonic sequence number of this observation.
pub fn TraceEntry::sequence(self : TraceEntry) -> Int {
self.sequence
}
///|
/// Application-provided node label.
pub fn TraceEntry::label(self : TraceEntry) -> String {
self.label
}
///|
/// Status returned by the node.
pub fn TraceEntry::status(self : TraceEntry) -> Status {
self.status
}
///|
/// Number of values visible when the node returned.
pub fn TraceEntry::blackboard_size(self : TraceEntry) -> Int {
self.blackboard_size
}
///|
/// Convert a trace entry into a stable CSV row.
pub fn TraceEntry::to_csv(self : TraceEntry) -> String {
"\{self.sequence},\{self.label},\{self.status.to_string()},\{self.blackboard_size}"
}
///|
/// Aggregate counts for the current trace window.
pub struct TraceSummary {
total : Int
successes : Int
failures : Int
running : Int
}
///|
/// Total observations in the summary.
pub fn TraceSummary::total(self : TraceSummary) -> Int {
self.total
}
///|
/// Successful observations in the summary.
pub fn TraceSummary::successes(self : TraceSummary) -> Int {
self.successes
}
///|
/// Failed observations in the summary.
pub fn TraceSummary::failures(self : TraceSummary) -> Int {
self.failures
}
///|
/// Running observations in the summary.
pub fn TraceSummary::running(self : TraceSummary) -> Int {
self.running
}
///|
/// A bounded FIFO trace window.
pub struct TraceBuffer {
entries : Array[TraceEntry]
capacity : Int
next_sequence : Ref[Int]
}
///|
/// Create a trace buffer. Non-positive capacities are normalized to one.
pub fn TraceBuffer::new(capacity : Int) -> TraceBuffer {
{
entries: [],
capacity: if capacity > 0 {
capacity
} else {
1
},
next_sequence: Ref::new(1),
}
}
///|
/// Maximum number of entries retained by this buffer.
pub fn TraceBuffer::capacity(self : TraceBuffer) -> Int {
self.capacity
}
///|
/// Number of entries currently retained.
pub fn TraceBuffer::size(self : TraceBuffer) -> Int {
self.entries.length()
}
///|
/// Return whether the trace window has no observations.
pub fn TraceBuffer::is_empty(self : TraceBuffer) -> Bool {
self.entries.length() == 0
}
///|
/// Record one node result and evict the oldest entry when full.
pub fn TraceBuffer::record(
self : TraceBuffer,
label : String,
status : Status,
bb : Blackboard,
) -> Unit {
if self.entries.length() >= self.capacity {
let _ = self.entries.remove(0)
}
self.entries.push({
sequence: self.next_sequence.get(),
label,
status,
blackboard_size: bb.size(),
})
self.next_sequence.set(self.next_sequence.get() + 1)
}
///|
/// Return retained entries from oldest to newest.
pub fn TraceBuffer::entries(self : TraceBuffer) -> Array[TraceEntry] {
let result : Array[TraceEntry] = []
for entry in self.entries {
result.push(entry)
}
result
}
///|
/// Return the newest retained entry, if any.
pub fn TraceBuffer::last(self : TraceBuffer) -> TraceEntry? {
if self.entries.length() == 0 {
None
} else {
Some(self.entries[self.entries.length() - 1])
}
}
///|
/// Remove all observations while keeping sequence numbers monotonic.
pub fn TraceBuffer::clear(self : TraceBuffer) -> Unit {
self.entries.clear()
}
///|
/// Count observations with one status in the current window.
pub fn TraceBuffer::count_status(self : TraceBuffer, expected : Status) -> Int {
let mut count = 0
for entry in self.entries {
if entry.status == expected {
count = count + 1
}
}
count
}
///|
/// Summarize the currently retained window.
pub fn TraceBuffer::summary(self : TraceBuffer) -> TraceSummary {
let mut successes = 0
let mut failures = 0
let mut running = 0
for entry in self.entries {
match entry.status {
Status::BTSuccess => successes = successes + 1
Status::BTFailure => failures = failures + 1
Status::BTRunning => running = running + 1
}
}
{ total: self.entries.length(), successes, failures, running }
}
///|
/// Export a stable header and all retained observations as CSV.
pub fn TraceBuffer::to_csv(self : TraceBuffer) -> String {
let output = StringBuilder::new()
output.write_string("sequence,label,status,blackboard_size\n")
for entry in self.entries {
output.write_string(entry.to_csv())
output.write_string("\n")
}
output.to_string()
}
///|
/// Wrap a node and record every returned status in a bounded trace.
pub fn trace_buffer_node(
label : String,
child : Node,
trace : TraceBuffer,
) -> Node {
let tick = fn(bb) {
let status = child.tick(bb)
trace.record(label, status, bb)
status
}
let reset = fn() { child.reset() }
Node::new(tick, reset)
}
///|
/// Record a completed tree-runner frame with a standard label.
pub fn trace_runner_tick(runner : TreeRunner, trace : TraceBuffer) -> Status {
let status = runner.tick()
trace.record("TreeRunner", status, runner.blackboard())
status
}
///|
/// Export only entries whose status matches the requested value.
pub fn TraceBuffer::filter_status(
self : TraceBuffer,
expected : Status,
) -> Array[TraceEntry] {
let result : Array[TraceEntry] = []
for entry in self.entries {
if entry.status == expected {
result.push(entry)
}
}
result
}
///|
/// Return the first retained entry with the supplied label.
pub fn TraceBuffer::find_label(
self : TraceBuffer,
label : String,
) -> TraceEntry? {
for entry in self.entries {
if entry.label == label {
return Some(entry)
}
}
None
}
///|
/// Return the total number of observations recorded since construction.
pub fn TraceBuffer::recorded_total(self : TraceBuffer) -> Int {
self.next_sequence.get() - 1
}
///|
/// Return whether the buffer has reached its retention limit.
pub fn TraceBuffer::is_full(self : TraceBuffer) -> Bool {
self.entries.length() >= self.capacity
}