Debug Logging

I needed a way to see what documents are doing in production without paying for it when nobody's watching. @debug is the answer — a built-in statement that publishes formatted messages from running documents, but only actually does the formatting work when someone is subscribed. Zero overhead otherwise.

The @debug Statement

@debug("simple message with no args");
@debug("player {0} scored {1} points", player_name, score);

The first argument is always a string literal format string. Additional arguments get substituted at positions marked by {N} placeholders.

Format Specifiers

Two forms:

Specifier Description Example
{N} Insert argument N as a string (0-indexed) @debug("value is {0}", x)
{N|zpK} Zero-pad argument N to K digits @debug("id = {0|zp6}", id) produces id = 000042
int score = 42;
string name = "Alice";

@debug("Player {0} has score {1}", name, score);
// Output: "Player Alice has score 42"

@debug("Padded score: {0|zp10}", score);
// Output: "Padded score: 0000000042"

Supported Argument Types

These types work as @debug arguments:

  • string, int, long, double, bool
  • message, dynamic, enum, principal
  • Vector types: vec2, vec3, vec4
  • Matrix types: mat2, mat3, mat4, math4

Tables, records, and other complex types are not supported — you'll get a compile-time error. I could have made them work by serializing to JSON, but that would undermine the zero-cost guarantee.

Compile-Time Validation

The format string is validated at compile time. The compiler catches:

  • Unclosed braces: @debug("unclosed {0", x) — error
  • Out-of-range indices: @debug("{0} {2}", x) — error (index 2 with only 1 argument)
  • Unknown format specifiers: @debug("{0|badspec}", x) — error
  • Non-convertible types: @debug("table={0}", my_table) — error

No runtime surprises. If it compiles, the format string is valid.

Debug Policy

You control who can subscribe to debug output by defining a debug policy in the @static block:

@static {
  create { return true; }
  debug { return @who.isAdamaDeveloper(); }
}

The debug policy:

  • Receives the subscriber's identity via @who
  • Returns true to allow the subscription, false to deny
  • Is evaluated at subscription time
  • Defaults to false (deny all) if not defined

Debug output is off by default. You have to explicitly opt in via policy. I'd rather err on the side of silence than accidentally leak internal state.

Performance

This is the part I care most about. @debug is designed for zero overhead when nobody's listening:

  • If no debug subscribers are connected, the format string is never evaluated and no allocations occur
  • The format string is parsed and validated at compile time, so runtime formatting is efficient
  • Arguments are only converted to strings when a subscriber exists

This means you can leave @debug statements in production code without guilt. They cost nothing until someone subscribes.

Usage Patterns

Tracing State Machine Transitions

#processing {
  @debug("entering #processing, round={0}, items={1}", round, items.size());
  // ... do work ...
  transition #waiting;
}

#waiting {
  @debug("entering #waiting");
}

Monitoring Channel Activity

channel place_bet(BetMsg msg) {
  @debug("bet from {0}: amount={1}", @who, msg.amount);
  // ... process bet ...
}

Tracking Agent Behavior

agent Helper {
  max_tool_rounds = 5;

  @description("Search for information")
  tool<SearchInput, SearchOutput> search {
    @debug("agent tool search called: query={0}", request.query);
    return { result: "..." };
  }
}

Debugging Formulas and Computations

procedure recalculate() {
  @debug("recalculating: x={0}, y={1}, sum={2}", x, y, x + y);
  // ... computation ...
}
Previous Agents
Next Stdlib