engine/baml-compiler/src/watch/README.md
The emit system allows BAML functions to publish real-time events during execution, enabling clients to observe intermediate values, streaming LLM responses, and control flow progress.
The emit system has three main components:
emit.rs) - Compile-time analysis to determine what channels a function providesemit_event.rs) - Runtime event structures for different kinds of emissionsthir/interpret.rs) - Runtime event firing during function executionA channel is a named stream of events that clients can subscribe to. Channels are:
int, string, MyClass)name parameterVariables are marked for emission using the @emit decorator:
function MyFunction() -> int {
let x: int = 10 @emit; // Simple emission
let y: string = "hello" @emit(name=msg); // Custom channel name
let z = LLMCall("...") @emit; // Streaming emission
x
}
There are two types of channels:
@emit decorators on variablesEach channel is identified by:
name: The terminal name (variable name or custom name)type: Variable or MarkdownHeadernamespace: Optional subfunction name (for transitive emissions)Example FQNs:
{ name: "x", type: Variable, namespace: None } - Direct emission{ name: "x", type: Variable, namespace: Some("ChildFn") } - Emission from subfunctionflowchart TD
A[BAML Source] --> B[Parse to AST]
B --> C[Lower to THIR]
C --> D[EmitChannels::analyze_program]
D --> E[Walk all functions]
E --> F[Find @emit decorators]
E --> G[Find markdown headers]
E --> H[Compute transitive closures]
F --> I[Create Variable channels]
G --> J[Create MarkdownHeader channels]
H --> K[Add namespaced channels]
I --> L[FunctionChannels]
J --> L
K --> L
L --> M[Type Generation]
M --> N[Client Code]
Key Steps:
Metadata Collection - For each function, collect:
@emit variables with their typesTransitive Closure - Compute which functions transitively call which other functions
Channel Synthesis - For each function, create channels for:
Type Unification - If a variable is reassigned multiple times with different types, the channel type becomes a union:
let x: int = 1 @emit(name=value);
x = "hello"; // Channel "value" becomes int | string
sequenceDiagram
participant Client
participant Runtime
participant Interpreter
participant LLMHandler
Client->>Runtime: call_function(params, emit_handler)
Runtime->>Interpreter: interpret_thir(llm_handler, emit_handler)
Note over Interpreter: Execute statements
Interpreter->>Interpreter: let x = 10 @emit
Interpreter->>Client: emit_handler(VarEvent{x: 10})
Interpreter->>Interpreter: let y = LLMCall(...) @emit
Interpreter->>Interpreter: Create EmitStreamContext
Interpreter->>LLMHandler: llm_handler("LLMCall", args, context)
LLMHandler->>Client: emit_handler(StreamStart{y, stream_id})
loop For each chunk
LLMHandler->>Client: emit_handler(StreamUpdate{y, stream_id, chunk})
end
LLMHandler->>Client: emit_handler(StreamEnd{y, stream_id})
LLMHandler-->>Interpreter: final_result
Interpreter->>Client: emit_handler(VarEvent{y: final_result})
Interpreter-->>Runtime: return_value
Runtime-->>Client: FunctionResult
For simple variable assignments:
EmitEvent {
value: EmitBamlValue::Value(baml_value_with_meta),
variable_name: Some("x"),
function_name: "MyFunction",
is_stream: false,
}
Fired when:
@emit is assignedFor LLM function calls and other streaming sources:
StreamStart - Fired before streaming begins:
EmitEvent {
value: EmitBamlValue::StreamStart(stream_id),
variable_name: Some("story"),
function_name: "MyFunction",
is_stream: true,
}
StreamUpdate - Fired for each chunk:
EmitEvent {
value: EmitBamlValue::StreamUpdate(stream_id, chunk_value),
variable_name: Some("story"),
function_name: "MyFunction",
is_stream: true,
}
StreamEnd - Fired after streaming completes:
EmitEvent {
value: EmitBamlValue::StreamEnd(stream_id),
variable_name: Some("story"),
function_name: "MyFunction",
is_stream: true,
}
The stream_id is a correlation ID (format: {function}_{variable}_{timestamp}) that allows clients to match the three lifecycle events.
For markdown header tracking (future feature):
EmitEvent {
value: EmitBamlValue::Block(header_label),
variable_name: None,
function_name: "MyFunction",
is_stream: false,
}
Each emitted value includes rich metadata:
pub struct EmitValueMetadata {
pub constraints: Vec<Constraint>, // Validation constraints
pub response_checks: Vec<ResponseCheck>, // LLM parsing checks
pub completion: Completion, // Streaming completion state
pub r#type: TypeIR, // Runtime type information
}
This allows clients to:
const listener = events.MyFunction();
// Subscribe to a variable channel
listener.on_var("x", (event) => {
console.log(event.value); // Typed as number
});
// Subscribe to a streaming channel
listener.on_stream("story", async (event) => {
// event.value is BamlStream<string, string>
for await (const chunk of event.value) {
console.log("Chunk:", chunk);
}
});
// Execute with listener
const result = await b.MyFunction({ events: listener });
The TypeScript client maintains a registry to correlate stream events:
const activeStreams = new Map<StreamId, {
stream: EmitStream,
handlers: StreamHandler[]
}>();
// On StreamStart: Create stream and fire handler
const stream = new EmitStream();
activeStreams.set(event.streamId, {stream, handlers});
fireHandlers(VarEvent{value: stream});
// On StreamUpdate: Push to existing stream
activeStreams.get(event.streamId).stream.pushValue(chunk);
// On StreamEnd: Complete and cleanup
activeStreams.get(event.streamId).stream.complete();
activeStreams.delete(event.streamId);
For LLM streaming to work, the interpreter must pass variable name context to the runtime:
let story = LLMCall(...) @emitEmitStreamContext { variable_name: "story", stream_id: "..." }This architectural decision allows the runtime to emit properly-named stream events even though it doesn't naturally know about BAML variable names.
The channel analysis ensures:
The @emit decorator supports several options:
let x = 10 @emit(
name="custom_name", // Custom channel name (default: variable name)
when=SomeFunction // Conditional emission (future feature)
);
Controls when auto-emission occurs:
EmitWhen::True (default) - Always emitEmitWhen::False - Manual emission onlyEmitWhen::FunctionName(fn) - Conditional based on function resultwhen parameter to conditionally emit based on runtime valuesSee baml-compiler/tests/validation_files/functions_v2/tests/ for examples:
field_level_assertions_v2.baml - Constraint checking with emitvalid_tests.baml - Basic emit functionalityfailing_tests.baml - Error handlingIntegration tests in integ-tests/typescript/tests/emit.test.ts demonstrate end-to-end usage.
baml-compiler/src/emit.rs - Channel analysisbaml-compiler/src/emit/emit_event.rs - Event structuresbaml-compiler/src/emit/emit_options.rs - Decorator parsingbaml-compiler/src/thir/interpret.rs - Event firing (search for emit_handler)baml-runtime/src/async_interpreter_runtime.rs - Runtime integration*/baml_client/events.ts - TypeScript integration