diff --git a/fern/01-guide/05-baml-advanced/runtime-events.mdx b/fern/01-guide/05-baml-advanced/runtime-events.mdx new file mode 100644 index 0000000000..77d9c9bfac --- /dev/null +++ b/fern/01-guide/05-baml-advanced/runtime-events.mdx @@ -0,0 +1,1448 @@ +--- +title: Runtime Events +--- + + +This feature was added in 0.210.0 + + +When running multi-step workflows, you need to be able to get information about +the running workflow. You might need this information to show incremental +results to your app’s users, or to debug a complex workflow combining multiple +LLM calls. + +BAML makes this possible though an event system that connects variables in your +BAML Workflow code to the Python/TypeScript/etc client code that you used to +invoke the workflow. + +## Using Markdown blocks to track execution + +Markdown Blocks are automatically tracked when you run BAML +workflows, and your client code can track which block is currently executing. In +the following example, your client can directly use the markdown headers to +render the current status on a status page: + +```baml BAML +struct Post { + title string + content string +} + +// Browse a URL and produce a number of posts describing +// its what was found there for our marketing site. +function MakePosts(source_url: string, count: int) -> Post[] { + # Summarize Source + let source = LLMSummarizeSource(source_url); + + # Determine Topic + let topic = LLMInferTopic(source); + + # Generate Marketing Post Ideas + let ideas: string[] = LLMIdeas(topic, source); + + # Generate posts + let posts: Post[] = []; + for (idea in ideas) { + + ## Create the post + let post = LLMGeneratePost(idea, source); + + ## Quality control + let quality = LLMJudgePost(post, idea, source); + if (quality > 8) { + posts.push(post); + } + } +} +``` + +## Track Emitted Block Events + +You can track emitted block events from your client code. + +When you generate client code from your BAML code, we produce listener structs +that allow you to hook in to events. + + + + +In your client code, you can bind events to callbacks: + +```python Python + # baml_client/events.py +from typing import TypeVar, Generic, Callable, Union + +class BlockEvent: + """ + Event fired when entering or exiting a markdown block + """ + block_label: str + event_type: str # "enter" | "exit" + +class MakePostsEventCollector: + """Event collector for MakePosts function""" + + def on_block(self, handler: Callable[[BlockEvent], None]) -> None: + """Register a handler for block events""" + pass +``` + +```python Python + # app.py + from baml_client.sync_client import { b } + from baml_client.types import Event + import baml_client.events + + def Example(): + # Get an Events callback collector with the right type + # for your MakePosts() function. + ev = events.MakePostsCollector() + + # Associate the block event with your own callback. + events.on_block(lambda ev: print(ev.block_label)) + + # Invoke the function. + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) + print(posts) +``` + + + +In your client code, you can bind events to callbacks: + +```typescript +// baml_client/event.ts +export interface BlockEvent { + block_label: string; + event_type: "enter" | "exit"; +} + +export interface MakePostsEventCollector { + // Register a handler for block events. + on_block(handler: (ev: BlockEvent) => void): void; +} +``` + +```typescript + // index.ts + import { b, events } from "./baml-client" + import type { Event } from "./baml-client/types" + + async function Example() { + // Get an Events callback collector with the right type + // for your MakePosts() function. + let ev = events.MakePosts() + + // Associate the block event with your own callback. + events.on_block((ev) => { + console.log(ev.block_label) + }); + + // Invoke the function. + const posts = await b.MakePosts( + "https://wikipedia.org/wiki/DNA", + {"events": ev} + ) + console.log(posts) + } +``` + + + +In your client code, you can consume events via channels: + +```go +// baml_client/events.go +package events + +type BlockEvent struct { + BlockLabel string `json:"block_label"` + EventType string `json:"event_type"` // "enter" | "exit" +} + +type MakePostsEventCollector struct { + blockEvents chan BlockEvent + // ... additional event channels are initialized elsewhere. +} + +func NewMakePostsEventCollector() *MakePostsEventCollector { + return &MakePostsEventCollector{ + blockEvents: make(chan BlockEvent, 100), + } +} + +// BlockEvents provides block execution updates as a channel. +func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent { + return c.blockEvents +} +``` + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" +) + +func main() { + ctx := context.Background() + + // Get an event collector with the right channels + // for your MakePosts() function. + ev := events.NewMakePostsEventCollector() + + // Consume block events asynchronously so updates are printed as they arrive. + go func() { + for blockEvent := range ev.BlockEvents() { + fmt.Println(blockEvent.BlockLabel) + } + }() + + // Invoke the function. + posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%+v\n", posts) +} +``` + + + +## Track variables with `emit` + +Variable updates can also be tracked with events. To mark an update for tracking, +attach `@emit` to the variable declaration (or update its options) so the runtime knows to emit changes. + +```baml BAML +let foo = State { counter: 0 } @emit; +foo.counter += 1; // *** This triggers an event +``` + + + ```python + events.on("foo", lambda st: my_state_update_handler(st)) + ``` + + ```typescript + events.on("foo", (st) => my_state_update_handler(st)) + ``` + + ```go + // Consume events from the events.on_foo channel + for st := range events.FooEvents { + handleState(st) + } + ``` + + +Updates can be tracked automatically or manually, depending on the `@emit` options you choose. +Automatic tracking will emit events any time a variable is updated. Manual tracking +only emits events after updates that you specify. + +### Auto Tracking + + + +Let’s see how we would use this capability to automatically track the progress of our +marketing post +generation workflow: + +```baml BAML +function MakePosts(source_url: string) -> Post[] { + let source = LLMSummarizeSource(source_url); + let topic = LLMInferTopic(source); + let ideas: string[] = LLMIdeas(topic, source); + let posts_target_length = ideas.len(); + + let progress_percent: int = 0 @emit; // *** Emit marker used here. + + let posts: Post[] = []; + for ((i,idea) in ideas.enumerate()) { + let post = LLMGeneratePost(idea, source); + let quality = LLMJudgePost(post, idea, source); + if (quality > 8) { + posts.push(post); + } else { + posts_target_length -= 1; + } + // *** This update will trigger events visible to the client. + progress_percent = i * 100 / posts_target_length + } +} +``` + +### Emit parameters + +The variable tracking can be controled in several ways. + + - `@emit(when=MyFilterFunc)` - Only emits when `MyFilterFunc` returns `true` + - `@emit(when=false)` - Never auto emit (only emit when manually triggered) + - `@emit(skip_def=true)` - Emits every time the variable is updated, but not on initialization + - `@emit(name=my_channel)` - Emits events on a channel you spceify (default is the variable name) + +The filter functions you pass to `when` should take two parameters. It will be called every +time an value is updated. The first parameter is the previous version of the value, and the +second is the new version. With these two parameters, you can determine whether the event should +be emitted or not (often by comparing the current to the previous, for deduplication). + +If you do not specify a filter function, BAML deduplicates automatically emitted events for you. +You could replicate the same behavior by using `@emit(when=MyFilterFunc)` where `MyFilterFunc` +is defined as: + +```baml BAML +function MyFilterFunc(prev: MyObject, curr: MyObject) -> bool { + !(prev.id() == curr.id()) || !(baml.deep_eq(prev, curr)) +} +``` + +### Manual Tracking + +Sometimes you want no automatic tracking at all. For example, if you are building up a complex +value in multiple steps, you may not want your application to see that value while it is still +under construction. In that case, use `@emit(when=false)` when initializing the variable, and +call `.$emit()` on the variable when you want to manually trigger an event. + + +```baml BAML +function ConstructValue(description: string) -> Character { + let character = Character { name: "", age: 0, skills: [] } @emit(when=false); + character.name = LLMChooseName(description); + character.age = LLMChooseAge(description); + character.skills = LLMChooseSkills(description); + character.$emit() // *** Only emit when done building the character. +} +``` + +### Sharing a Channel + +Sometimes you want multiple variables to send update events on the same channel, for example, +if you want a single view of all the state updates from multiple values in your BAML code, +because you will render them into a single view in the order that they are emitted. + +```baml BAML +function DoWork() -> bool { + let status = "Starting" @emit(name=updates); + let progress = 0 @emit(name=updates, skip_def=true); + for (let i = 0; i < 100; i++) { + progress = i; // *** These updates will apear on the `updates` channel. + } + status = "Done"; + return true; +} +``` + +## Receiving Events from Client Code + + + + + +When you generate a BAML client for our original function, your Python SDK will +include a `MakePostsEventCollector` class. This class contains configurable callbacks +for all your tracked variables. For example, it contains callbacks for `progress_percent` +because we marked that variable with `@emit`. The callbacks will receive an `int` data payload, +because `progress_percent` is an `int`. + +```python +# baml_client/events.py + +T = TypeVar('T') + +class VarEvent(Generic[T]): + """ + Event fired when an emitted variable is updated + """ + value: T + timestamp: str + function_name: str + +class MakePostsVarsCollector: + progress_percent: Callable[[VarEvent[int]], None] + +class MakePostsEventCollector: + """Event collector for MakePosts function""" + vars: MakePostsVarsCollector +``` + +```python +# app.py +from baml_client.sync_client import { b } +from baml_client.types import Event +import baml_client.events + +def Example(): + # Get an Events callback collector with the right type + # for your MakePosts() function. + ev = events.MakePosts() + + # Track the progress_percent variable updates + events.vars.on_progress_percent(lambda percent: print(f"Progress: {percent}%")) + + # Invoke the function. + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) + print(posts) +``` + + + + +When you generate a BAML client for this function, its `MakePostsEventCollector` +will accept callbacks for `progress_percent` because we marked that variable with +`@emit`, and the callbacks will receive an `int` data payload, because +`progress_percent` is an `int`. + +```typescript +// baml_client/events.ts +import { VarEvent } from "./types" + +export interface MakePostsEventCollector { + on_var_progress_percent(callback: (percent: number) => void): void +} + +export function MakePosts(): MakePostsEventCollector { + return { + on_var_progress_percent(callback: (percent: number) => void): void { + // Implementation details + } + } +} +``` + +```typescript +// index.ts +import { b, events } from "./baml-client" +import type { VarEvent } from "./baml-client/types" + +async function Example() { + // Get an Events callback collector with the right type + // for your MakePosts() function. + let ev = events.MakePosts() + + // Track the progress_percent variable updates + events.on_progress_percent((percent) => { + console.log(`Progress: ${percent}%`) + }); + + // Invoke the function. + const posts = await b.MakePosts( + "https://wikipedia.org/wiki/DNA", + {"events": ev } + ) + console.log(posts) +} +``` + + + + +In your client code, you can track these emitted variables by constructing the +generated event collector and reading from the channels it exposes. + +```go +// baml_client/events.go +package events + +import "time" + +type BlockEvent struct { + BlockLabel string `json:"block_label"` + EventType string `json:"event_type"` // "enter" | "exit" + Timestamp time.Time `json:"timestamp"` +} + +type VarEvent[T any] struct { + VariableName string `json:"variable_name"` + Value T `json:"value"` + Timestamp time.Time `json:"timestamp"` + FunctionName string `json:"function_name"` +} + +type MakePostsEventCollector struct { + blockEvents chan BlockEvent + progressPercentEvents chan VarEvent[int] +} + +func NewMakePostsEventCollector() *MakePostsEventCollector { + return &MakePostsEventCollector{ + blockEvents: make(chan BlockEvent, 100), + progressPercentEvents: make(chan VarEvent[int], 100), + } +} + +// BlockEvents returns block execution updates. +func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent { + return c.blockEvents +} + +// ProgressPercentEvents streams progress_percent variable updates. +func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] { + return c.progressPercentEvents +} +``` + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" +) + +func main() { + ctx := context.Background() + + // Get an event collector with the right channels + // for your MakePosts() function. + ev := events.NewMakePostsEventCollector() + + // Consume block events and progress updates concurrently. + go func() { + for block := range ev.BlockEvents() { + fmt.Printf("Block: %s\n", block.BlockLabel) + } + }() + + go func() { + for percent := range ev.ProgressPercentEvents() { + fmt.Printf("Progress: %d%%\n", percent.Value) + } + }() + + // Invoke the function. + posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%+v\n", posts) +} +``` + + + +For details about the types of events, see [BAML Language Reference](/ref/baml_client/events) + +# Streaming + +If updates to variables tagged with `@emit` include large amounts of data that you want to start +surfacing to your application before they are done being generated, you want to use +the streaming event interface. Streaming events are available for all `@emit` variables, +but they are generally only useful when assigning a variable from the result of +an LLM function. All other streamed events will return their values in a single +complete chunk. + +```baml BAML +function DescribeTerminatorMovies() -> string[] { + let results = []; + for (x in [1,2,3]) { + let movie_text = LLMElaborateOnTopic("Terminator " + std.to_string(x)) @emit; + results.push(movie_text); + } + return results; +} + +function LLMElaborateOnTopic(topic: string) -> string { + client GPT4 + prompt #" + Write a detailed 500-word analysis of {{ topic }}. + Include plot summary, themes, and cultural impact. + "# +} +``` + +This function will take a while to run because it calls an LLM function +three times. However, you can stream the results of each of these calls +to start getting immediate feedback from the workflow as the LLM generates +text token by token. + +The streaming listeners are available in client code under a separate streaming +module that mirrors the structure of the regular event collectors. + + + + +```python +# baml_client/events.py +from typing import TypeVar, Generic, Callable +from baml_client.types import BamlStream, VarEvent + +T = TypeVar('T') + +class VarEvent(Generic[T]): + """ + Event fired when an emitted variable is updated + """ + variable_name: str + value: T + timestamp: str + function_name: str + +class BlockEvent: + """ + Event fired when entering or exiting a markdown block + """ + block_label: str + event_type: str # "enter" | "exit" + +class MakePostsVarsCollector: + progress_percent: Callable[[VarEvent[int]], None] + +class DescribeTerminatorMoviesEventCollector: + """Event collector for DescribeTerminatorMovies function with both regular and streaming events""" + + def on_block(self, handler: Callable[[BlockEvent], None]) -> None: + """Register a handler for block events""" + pass + + def on_var_movie_text(self, handler: Callable[[VarEvent[str]], None]) -> None: + """Register a handler for movie_text variable updates""" + pass + + def on_stream_movie_text(self, handler: Callable[[BamlStream[VarEvent[str]]], None]) -> None: + """Register a handler for streaming movie_text variable updates""" + pass +``` + +```python +# app.py +from baml_client.sync_client import b +import baml_client.events as events + +def example(): + # Create the unified event collector + ev = events.DescribeTerminatorMoviesEventCollector() + + # Track streaming updates for the main emitted variable + def handle_movie_text_stream(stream): + for event in stream: + print(f"Streaming movie text: {event.value}") + + ev.on_stream_movie_text(handle_movie_text_stream) + + # Invoke the function with events + results = b.DescribeTerminatorMovies({"events": ev}) + print("Final results:", results) +``` + + + + +```typescript +// baml_client/events.ts +import { BamlStream, VarEvent } from "./types"; + +export interface BlockEvent { + block_label: string; + event_type: "enter" | "exit"; +} + +export interface VarEvent { + variable_name: string; + value: T; + timestamp: string; + function_name: string; +} + +export interface DescribeTerminatorMoviesEventCollector { + // Regular event handlers + on_block(handler: (ev: BlockEvent) => void): void; + on_var_movie_text(handler: (ev: VarEvent) => void): void; + + // Streaming event handlers + on_stream_movie_text(handler: (stream: BamlStream>) => void): void; +} + +export function DescribeTerminatorMovies(): DescribeTerminatorMoviesEventCollector { + return { + on_block(handler: (ev: BlockEvent) => void): void { + // Implementation details + }, + on_var_movie_text(handler: (ev: VarEvent) => void): void { + // Implementation details + }, + on_stream_movie_text(handler: (stream: BamlStream>) => void): void { + // Implementation details + } + } +} +``` + +```typescript +// index.ts +import { b, events } from "./baml-client" + +async function example() { + // Create the unified event collector + let ev = events.DescribeTerminatorMovies() + + // Track streaming updates for the main emitted variable + ev.on_stream_movie_text(async (stream) => { + for await (const event of stream) { + console.log(`Streaming movie text: ${event.value}`) + } + }) + + // Invoke the function with events + const results = await b.DescribeTerminatorMovies({"events": ev}) + console.log("Final results:", results) +} +``` + + + + +```go +// baml_client/events.go +package events + +import "time" + +type BlockEvent struct { + BlockLabel string `json:"block_label"` + EventType string `json:"event_type"` // "enter" | "exit" + Timestamp time.Time `json:"timestamp"` +} + +type VarEvent[T any] struct { + VariableName string `json:"variable_name"` + Value T `json:"value"` + Timestamp time.Time `json:"timestamp"` + FunctionName string `json:"function_name"` +} + +type DescribeTerminatorMoviesEventCollector struct { + blockEvents chan BlockEvent + movieTextEvents chan VarEvent[string] + movieTextStreams chan (<-chan VarEvent[string]) +} + +func NewDescribeTerminatorMoviesEventCollector() *DescribeTerminatorMoviesEventCollector { + return &DescribeTerminatorMoviesEventCollector{ + blockEvents: make(chan BlockEvent, 100), + movieTextEvents: make(chan VarEvent[string], 100), + movieTextStreams: make(chan (<-chan VarEvent[string]), 10), + } +} + +func (c *DescribeTerminatorMoviesEventCollector) BlockEvents() <-chan BlockEvent { + return c.blockEvents +} + +func (c *DescribeTerminatorMoviesEventCollector) MovieTextEvents() <-chan VarEvent[string] { + return c.movieTextEvents +} + +// MovieTextStreams produces a stream-of-streams for emitted movie_text updates. +func (c *DescribeTerminatorMoviesEventCollector) MovieTextStreams() <-chan (<-chan VarEvent[string]) { + return c.movieTextStreams +} +``` + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" +) + +func main() { + ctx := context.Background() + + // Create the unified event collector + ev := events.NewDescribeTerminatorMoviesEventCollector() + + // Track block events and single-value updates concurrently. + go func() { + for block := range ev.BlockEvents() { + fmt.Printf("Block: %s\n", block.BlockLabel) + } + }() + + go func() { + for movieText := range ev.MovieTextEvents() { + fmt.Printf("Variable movie text: %s\n", movieText.Value) + } + }() + + // Track streaming updates using the channel-of-channels pattern. + go func() { + for stream := range ev.MovieTextStreams() { + go func(inner <-chan events.VarEvent[string]) { + for event := range inner { + fmt.Printf("Streaming movie text: %s\n", event.Value) + } + }(stream) + } + }() + + // Invoke the function with events + results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Final results: %+v\n", results) +} +``` + + + +## Combining Regular Events and Streaming + +You can use both regular events and streaming events together in a single unified collector to get comprehensive observability: + + + + +```python +from baml_client.sync_client import b +import baml_client.events as events + +def comprehensive_example(): + # Create unified event collector + ev = events.DescribeTerminatorMoviesEventCollector() + + # Regular events for workflow progress + ev.on_block(lambda block: print(f"Block: {block.block_label}")) + ev.on_var_movie_text(lambda movie_text: print(f"Variable movie text: {movie_text.value}")) + + # Streaming events for real-time content + def handle_stream(stream): + for event in stream: + print(f"Streaming content: {event.value}") + + ev.on_stream_movie_text(handle_stream) + + # Use single events parameter + results = b.DescribeTerminatorMovies({"events": ev}) +``` + + + + +```typescript +import { b, events } from "./baml-client" + +async function comprehensiveExample() { + // Create unified event collector + let ev = events.DescribeTerminatorMovies() + + // Regular events for workflow progress + ev.on_block((block) => console.log(`Block: ${block.block_label}`)) + ev.on_var_movie_text((movieText) => console.log(`Variable movie text: ${movieText.value}`)) + + // Streaming events for real-time content + ev.on_stream_movie_text(async (stream) => { + for await (const event of stream) { + console.log(`Streaming content: ${event.value}`) + } + }) + + // Use single events parameter + const results = await b.DescribeTerminatorMovies({ events: ev }) +} +``` + + + + +```go +func comprehensiveExample() { + ctx := context.Background() + + // Create unified event collector + ev := events.NewDescribeTerminatorMoviesEventCollector() + + // Regular events for workflow progress + go func() { + for block := range ev.BlockEvents() { + fmt.Printf("Block: %s\n", block.BlockLabel) + } + }() + go func() { + for movieText := range ev.MovieTextEvents() { + fmt.Printf("Variable movie text: %s\n", movieText.Value) + } + }() + + // Streaming events for real-time content + go func() { + for stream := range ev.MovieTextStreams() { + go func(inner <-chan events.VarEvent[string]) { + for event := range inner { + fmt.Printf("Streaming content: %s\n", event.Value) + } + }(stream) + } + }() + + // Use single Events parameter + results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } +} +``` + + + +# Usage Scenarios + +## Track events from subfunctions + +When your main workflow calls other BAML functions, you can track events from those subfunctions as well. If `MakePosts()` calls `Foo()`, and `Foo()` contains variables tagged with `@emit` or markdown blocks, the client invoking `MakePosts()` can subscribe to those subfunction events through dedicated records in the `EventCollector`. + +Consider this example where `MakePosts()` calls a helper function: + +```baml BAML +function MakePosts(source_url: string) -> Post[] { + let posts = GeneratePostsWithProgress(source_url); + return posts; +} + +function GeneratePostsWithProgress(url: string) -> Post[] { + # Analyzing content + let content = LLMAnalyzeContent(url); + + let progress_status = "Starting generation" @emit; + + # Generate posts + let posts = []; + for (i in [1,2,3]) { + progress_status = "Generating post " + i.to_string(); + posts.push(LLMGeneratePost(content, i)); + } + + return posts; +} +``` + + + + +```python +# baml_client/events.py + +class GeneratePostsWithProgressEventCollector: + """Event collector for GeneratePostsWithProgress function""" + + def on_block(self, handler: Callable[[BlockEvent], None]) -> None: + """Register a handler for block events from this function""" + pass + + def on_var_progress_status(self, handler: Callable[[VarEvent[str]], None]) -> None: + """Register a handler for progress_status variable updates""" + pass + +class MakePostsEventCollector: + """Event collector for MakePosts function""" + + def __init__(self): + self.function_GeneratePostsWithProgress = GeneratePostsWithProgressEventCollector() +``` + +```python +# app.py +from baml_client.sync_client import b +import baml_client.events as events + +def example(): + # Create the main event collector + ev = events.MakePostsEventCollector() + + # Subscribe to subfunction events + ev.function_GeneratePostsWithProgress.on_var_progress_status( + lambda e: print(f"Subfunction progress: {e.value}") + ) + + ev.function_GeneratePostsWithProgress.on_block( + lambda e: print(f"Subfunction block: {e.block_label}") + ) + + # Invoke the function + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) + print(posts) +``` + + + + +```typescript +// baml_client/events.ts + +export interface GeneratePostsWithProgressEventCollector { + on_block(handler: (ev: BlockEvent) => void): void; + on_var_progress_status(handler: (ev: VarEvent) => void): void; +} + +export interface MakePostsEventCollector { + function_GeneratePostsWithProgress: GeneratePostsWithProgressEventCollector; +} + +export function MakePosts(): MakePostsEventCollector { + return { + function_GeneratePostsWithProgress: { + on_block(handler: (ev: BlockEvent) => void): void { + // Implementation details + }, + on_var_progress_status(handler: (ev: VarEvent) => void): void { + // Implementation details + } + } + } +} +``` + +```typescript +// index.ts +import { b, events } from "./baml-client" + +async function example() { + // Create the main event collector + let ev = events.MakePosts() + + // Subscribe to subfunction events + ev.function_GeneratePostsWithProgress.on_var_progress_status((e) => { + console.log(`Subfunction progress: ${e.value}`) + }) + + ev.function_GeneratePostsWithProgress.on_block((e) => { + console.log(`Subfunction block: ${e.block_label}`) + }) + + // Invoke the function + const posts = await b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) + console.log(posts) +} +``` + + + + +```go +// baml_client/events.go +package events + +import "time" + +type BlockEvent struct { + BlockLabel string `json:"block_label"` + EventType string `json:"event_type"` + Timestamp time.Time `json:"timestamp"` +} + +type VarEvent[T any] struct { + VariableName string `json:"variable_name"` + Value T `json:"value"` + Timestamp time.Time `json:"timestamp"` + FunctionName string `json:"function_name"` +} + +type GeneratePostsWithProgressEventCollector struct { + blockEvents chan BlockEvent + progressStatusEvents chan VarEvent[string] +} + +func newGeneratePostsWithProgressEventCollector() *GeneratePostsWithProgressEventCollector { + return &GeneratePostsWithProgressEventCollector{ + blockEvents: make(chan BlockEvent, 100), + progressStatusEvents: make(chan VarEvent[string], 100), + } +} + +func (c *GeneratePostsWithProgressEventCollector) BlockEvents() <-chan BlockEvent { + return c.blockEvents +} + +func (c *GeneratePostsWithProgressEventCollector) ProgressStatusEvents() <-chan VarEvent[string] { + return c.progressStatusEvents +} + +type MakePostsEventCollector struct { + blockEvents chan BlockEvent + progressPercentEvents chan VarEvent[int] + FunctionGeneratePostsWithProgress *GeneratePostsWithProgressEventCollector +} + +func NewMakePostsEventCollector() *MakePostsEventCollector { + return &MakePostsEventCollector{ + blockEvents: make(chan BlockEvent, 100), + progressPercentEvents: make(chan VarEvent[int], 100), + FunctionGeneratePostsWithProgress: newGeneratePostsWithProgressEventCollector(), + } +} + +func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent { + return c.blockEvents +} + +func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] { + return c.progressPercentEvents +} +``` + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" +) + +func main() { + ctx := context.Background() + + // Create the main event collector + ev := events.NewMakePostsEventCollector() + + // Consume subfunction streams as well as top-level updates. + go func() { + for block := range ev.FunctionGeneratePostsWithProgress.BlockEvents() { + fmt.Printf("Subfunction block: %s\n", block.BlockLabel) + } + }() + + go func() { + for status := range ev.FunctionGeneratePostsWithProgress.ProgressStatusEvents() { + fmt.Printf("Subfunction progress: %s\n", status.Value) + } + }() + + // Invoke the function + posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%+v\n", posts) +} +``` + + + +## Track values across names + +In JavaScript and Python, values can be referenced by multiple names, and +updating the value through one name will update it through the other names, +too. + +```python Python +x = { "name": "BAML" } # Make a python dict +y = x # Alias x to a new name +y["name"] = "Baml" # Modify the name +assert(x["name"] == "Baml") # The original value is updated +``` + +The same rule applies to variables tagged with `@emit`. Anything causing a change to a value will +cause the value to emit an event to listeners that subscribe to the original +variable. + +```baml BAML +let x = Foo { name: "BAML" } @emit; // Make a BAML value that auto-emits +let y = x; // Alias x to a new name +y.name = "Baml"; // Modify the new name => triggers event + +let a: int = 1 @emit; // Make a tracked BAML value +let b = a; // Alias a to a new name +b++; // Modify the new name => No new event + // (see Note below) +``` + + + Changes through a separate name for simple values like ints and strings, + on the other hand, wil not result in events being emitted, because when you + assign a new variable to an old variable holding plain data, the new variable + will receive a copy of the data, and modifying that copy will not affect + the original value. + + As a rule of thumb, if a change to the new variable causes a change to the + old value, then the original variable will emit an event. + + +## Track values that get packed into data structures + +If you put a value into a data structure, then modify it through that data structure, +the value will continue to emit an event. + +```baml BAML +let x = Foo { name: "BAML" } @emit; // Make a tracked BAML value +let y = [x]; // Pack x into a list +y[0].name = "Baml"; // Modify the list item => triggers event +``` + +Reminder: In Python and TypeScript, if you put a variable `x` into a list, then +modify it through the list, printing `x` will show the modified value. So +modifying `x` through `y[0]` above will also result in an event being emitted. + +## Track variables across function calls + +When you pass an `@emit` variable to a function, there are two possible outcomes +if the called function modifies the variable: + +1. The modifications will be remembered by the system, but only the final + change to the variable will be emitted, and that will only happen when + the function returns. **OR:** +1. The modification will immediately result in the firing of an event. + +You get to choose the behavior based on the needs of your workflow. If the function +is doing some setup work that makes multiple changes to the emitted value to build +it up to a valid result before the function returns, use Option 1 to hide the events +from all those intermediate states. But if the sub-function is part of a workflow +and you are using events to track all updates to your workflow's state, use Option 2 +to see all the intermediate updates in real time. + + + The event-emission behavior of Option 1 differs from the rule of thumb given + above about Python and TypeScript. + We offer two steparate options because there are legitimate cases where you would + not want the intermediate states to be emitted - for example if they violate + invariants of your type. + + +To choose between modes, annotate the parameter with `@emit` in the function signature. + +```baml BAML +function Main() -> int { + let state = Foo { + name: "BAML", + counter: 0, + } @emit; // Track state updates automatically + ReadState(state); + ChangeState(state); + 0 +} + +// This function uses Option 1, `state` in `Main()` will only fire one +// event, when the function returns, even through `s` is modified twice. +function ReadState(state: Foo) -> Foo { + state.counter++; + state.counter++; +} + +// This function uses Option 2, the `s` parameter is +// marked with `@emit`, so `state` in `Main()` will fire two events, +// one for each update of `s`. +function ChangeState(s: Foo @emit) -> null { + s.counter++; + s.name = "Baml"; +} +``` + +# Comparison with other event systems + +The `emit` system differs from many observability systems by focusing on automatic updates +and typesafe event listeners. The ability to generate client code from your BAML +programs is what allows us to create this tight integration. + +Let's compare BAML's observability to several other systems to get a better understanding +of the trade-offs. + +## Logging and printf debugging + +The most common way of introspecting a running program is to add logging statements in +your client's logging framework. Let's compare a simple example workflow in native +Python to one instrumented in BAML. + +```python Python +import logging +from typing import List, Dict, Any + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def LLMAnalizeSentiment(message: str) -> str: pass # Assumed function +def LLMSummarizeSentiments(sentiments: List[str]) -> str: pass # Assumed function + +class Response(BaseModel): + sentiments: string[] + summary: string + +def analyze_sentiments(phrases: List[str]) -> Response: + logger.info(f"Starting analysis of {len(phrases)} phrases") + + sentiments = [] + for i, phrase in enumerate(phrases, 1): + logger.info(f"Analyzing phrase {i}/{len(phrases)}") + sentiment = LLMAnalizeSentiment(phrase) + sentiments.append({"phrase": phrase, "sentiment": sentiment}) + + logger.info("Generating summary") + summary = LLMSummarizeSentiments([s["sentiment"] for s in sentiments]) + + logger.info("Analysis complete") + return Response(sentiments=sentiments, summary=summary) +``` + +With BAML's block events, we don't need to mix explicit logging with the workflow +logic. When a logged event needs extra context (such as the index of an item being +processed from a list), we can use an `@emit` variable. + +```baml BAML +function LLMAnalyzeSentiment(message: string) -> string { ... } +function LLMSummarizeSentiments(message: string) -> string { ... } + +class Response { + sentiments string[] + summary string +} + +function AnalyzeSentiments(messages: string[]) -> Response { + let status = "Starting analysis of " + messages.length().to_string() + " messages" @emit; + + sentiments = [] + for i, message in enumerate(messages, 1): + status = `Analyzing message ${i}/${messages.len()}` + sentiments.push(LLMAnalizeSentiment(message)) + + status = "Generating summary"; + summary = LLMSummarizeSentiments([s["sentiment"] for s in sentiments]) + + status = "Analysis complete" + return Response(sentiments=sentiments, summary=summary) +} +``` + +## Vercel AI SDK Generators + +In Vercel's AI SDK, you can use TypeScript generators to yield incremental updates during tool execution. The calling code can consume these yielded values to provide real-time feedback to users. + +```typescript TypeScript (Vercel AI SDK) +import { UIToolInvocation, tool } from 'ai'; +import { z } from 'zod'; + +export const weatherTool = tool({ + description: 'Get the weather in a location', + inputSchema: z.object({ city: z.string() }), + async *execute({ city }: { city: string }) { + yield { state: 'loading' as const }; + + // Add artificial delay to simulate API call + await new Promise(resolve => setTimeout(resolve, 2000)); + + const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy']; + const weather = + weatherOptions[Math.floor(Math.random() * weatherOptions.length)]; + + yield { + state: 'ready' as const, + temperature: 72, + weather, + }; + }, +}); +``` + +The calling code streams these incremental updates by consuming the generator chunks: + +```typescript TypeScript (Consuming streaming yields) +import { streamText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +const stream = streamText({ + model: openai('gpt-4'), + tools: { weather: weatherTool }, + messages: [{ role: 'user', content: 'What is the weather in New York?' }], +}); + +// Stream individual yielded values as they arrive +for await (const chunk of stream) { + if (chunk.type === 'tool-call-streaming-start') { + console.log('Weather tool started...'); + } else if (chunk.type === 'tool-result') { + // Each yield from the generator appears here + if (chunk.result.state === 'loading') { + console.log('Weather lookup in progress...'); + } else if (chunk.result.state === 'ready') { + console.log(`Weather: ${chunk.result.weather}, Temperature: ${chunk.result.temperature}°F`); + } + } else if (chunk.type === 'text-delta') { + // Stream the AI's text response + process.stdout.write(chunk.textDelta); + } +} +``` + +This pattern provides a great mix of streaming and type safety. It differs architecturally +from the pattern in BAML, where Workflow logic is separated from event handling logic. + +In BAML, functions and return values are meant for composing Workflow logic, while events +are meant for communicating state back to your application. In the AI SDK, return values +are used directly. + +**Key differences:** + +**Vercel AI SDK Generators:** +- Manual yield statements at specific points in your tool execution +- Generic streaming through the AI SDK's protocol +- Tool-level progress updates handled by the framework +- Updates tied to tool execution lifecycle + +**BAML's `emit`:** +- Automatic event generation from variable assignments +- Typesafe event listeners generated from your workflow code +- Fine-grained control over exactly what business logic gets tracked +- Updates tied to your specific domain logic and variable names + + +## Mastra `.watch()` + +Mastra provides a `.watch()` method for monitoring workflow execution in real-time. Let's compare a workflow monitoring example using Mastra's approach to one using BAML's `emit` system. + +```typescript TypeScript (Mastra) +// Mastra approach - watching workflow steps externally +const workflow = mastra.createWorkflow(...) +const run = await workflow.createRunAsync() + +run.watch((event) => { + console.log(`Step ${event?.payload?.currentStep?.id} completed`) + console.log(`Progress: ${event?.payload?.progress}`) +}) + +const result = await run.start({ inputData: { value: "initial data" } }) +``` + +With BAML's `emit` system, you mark variables directly in your workflow logic and get typesafe event listeners generated for you. + +Both approaches enable real-time workflow monitoring. But Mastra's `watch()` function contains +a more limited number of fields - telling you only about the Workflow stage you are in, not +specific values being processed. + +## Comparison Table + +| Feature | Printf | Mastra | BAML | +| --- | --- | --- | --- | +| Real-time | 🟢 | 🟢 | 🟢 | +| Streaming | ⚪ | 🟢 | 🟢 | +| Debug levels | 🟢 | ⚪ | ⚪ | +| Value subscription | ⚪ | ⚪ | 🟢 | +| Typed listeners | ⚪ | ⚪ | 🟢 | diff --git a/fern/03-reference/baml_client/runtime-events.mdx b/fern/03-reference/baml_client/runtime-events.mdx new file mode 100644 index 0000000000..3072491523 --- /dev/null +++ b/fern/03-reference/baml_client/runtime-events.mdx @@ -0,0 +1,444 @@ +--- +title: Runtime Events +--- + + +This feature was added in TODO + + +The BAML runtime events system allows you to receive real-time callbacks about workflow execution, including block progress and variable updates. This enables you to build responsive UIs, track progress, and access intermediate results during complex BAML workflows. + +## Event Types + +### VarEvent + +Represents an update to an emitted variable in your BAML workflow. + + + +```python +from typing import TypeVar, Generic +from baml_client.types import VarEvent + +T = TypeVar('T') + +class VarEvent(Generic[T]): + """ + Event fired when an emitted variable is updated + + Attributes: + variable_name: Name of the variable that was updated + value: The new value of the variable + timestamp: ISO timestamp when the update occurred + function_name: Name of the BAML function containing the variable + """ + variable_name: str + value: T + timestamp: str + function_name: str + +# Usage examples: +# VarEvent[int] for integer variables +# VarEvent[str] for string variables +# VarEvent[List[Post]] for complex types +``` + + + +```typescript +import type { VarEvent } from './baml-client/types' + +interface VarEvent { + /** + * Event fired when an emitted variable is updated + */ + + /** Name of the variable that was updated */ + variableName: string + + /** The new value of the variable */ + value: T + + /** ISO timestamp when the update occurred */ + timestamp: string + + /** Name of the BAML function containing the variable */ + functionName: string +} + +// Usage examples: +// VarEvent for integer variables +// VarEvent for string variables +// VarEvent for complex types +``` + + + +```go +package types + +import "time" + +// Since Go doesn't have user-defined generics, we generate specific types +// for each emitted variable in your BAML functions + +// For a variable named "progress_percent" of type int +type ProgressPercentVarEvent struct { + // Name of the variable that was updated + VariableName string `json:"variable_name"` + + // The new value of the variable + Value int `json:"value"` + + // Timestamp when the update occurred + Timestamp time.Time `json:"timestamp"` + + // Name of the BAML function containing the variable + FunctionName string `json:"function_name"` +} + +// For a variable named "current_task" of type string +type CurrentTaskVarEvent struct { + VariableName string `json:"variable_name"` + Value string `json:"value"` + Timestamp time.Time `json:"timestamp"` + FunctionName string `json:"function_name"` +} + +// For a variable named "completed_posts" of type []Post +type CompletedPostsVarEvent struct { + VariableName string `json:"variable_name"` + Value []Post `json:"value"` + Timestamp time.Time `json:"timestamp"` + FunctionName string `json:"function_name"` +} +``` + + + +### BlockEvent + +Represents progress through a markdown block in your BAML workflow. + + + +```python +from baml_client.types import BlockEvent + +class BlockEvent: + """ + Event fired when entering or exiting a markdown block + + Attributes: + block_label: The markdown header text (e.g., "# Summarize Source") + block_level: The markdown header level (1-6) + event_type: Whether we're entering or exiting the block + timestamp: ISO timestamp when the event occurred + function_name: Name of the BAML function containing the block + """ + block_label: str + block_level: int # 1-6 for # through ###### + event_type: str # "enter" | "exit" + timestamp: str + function_name: str +``` + + + +```typescript +import type { BlockEvent } from './baml-client/types' + +interface BlockEvent { + /** + * Event fired when entering or exiting a markdown block + */ + + /** The markdown header text (e.g., "# Summarize Source") */ + blockLabel: string + + /** The markdown header level (1-6) */ + blockLevel: number + + /** Whether we're entering or exiting the block */ + eventType: "enter" | "exit" + + /** ISO timestamp when the event occurred */ + timestamp: string + + /** Name of the BAML function containing the block */ + functionName: string +} +``` + + + +```go +package types + +import "time" + +type BlockEventType string + +const ( + BlockEventEnter BlockEventType = "enter" + BlockEventExit BlockEventType = "exit" +) + +type BlockEvent struct { + // The markdown header text (e.g., "# Summarize Source") + BlockLabel string `json:"block_label"` + + // The markdown header level (1-6) + BlockLevel int `json:"block_level"` + + // Whether we're entering or exiting the block + EventType BlockEventType `json:"event_type"` + + // Timestamp when the event occurred + Timestamp time.Time `json:"timestamp"` + + // Name of the BAML function containing the block + FunctionName string `json:"function_name"` +} +``` + + + +## Usage Examples + +### Tracking Variable Updates + + + +```python +from baml_client import b, events +from baml_client.types import VarEvent + +def track_progress(event: VarEvent[int]): + print(f"Progress updated: {event.value}% at {event.timestamp}") + +def track_current_task(event: VarEvent[str]): + print(f"Now working on: {event.value}") + +# Set up variable tracking +ev = events.MakePosts() +events.on_progress_percent(track_progress) +events.on_current_task(track_current_task) + +# Run the function +posts = await b.MakePosts("https://example.com", {"events": ev}) +``` + + + +```typescript +import { b, events } from './baml-client' +import type { VarEvent } from './baml-client/types' + +const trackProgress = (event: VarEvent) => { + console.log(`Progress updated: ${event.value}% at ${event.timestamp}`) +} + +const trackCurrentTask = (event: VarEvent) => { + console.log(`Now working on: ${event.value}`) +} + +// Set up variable tracking +const ev = events.MakePosts() +events.on_progress_percent(trackProgress) +events.on_current_task(trackCurrentTask) + +// Run the function +const posts = await b.MakePosts("https://example.com", { events: ev }) +``` + + + +```go +package main + +import ( + "fmt" + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/types" +) + +func trackProgress(event *types.ProgressPercentVarEvent) { + fmt.Printf("Progress updated: %d%% at %s\n", + event.Value, event.Timestamp.Format("15:04:05")) +} + +func trackCurrentTask(event *types.CurrentTaskVarEvent) { + fmt.Printf("Now working on: %s\n", event.Value) +} + +func main() { + ctx := context.Background() + + // Set up variable tracking + ev := events.NewMakePosts() + events.OnProgressPercent(trackProgress) + events.OnCurrentTask(trackCurrentTask) + + // Run the function + posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } +} +``` + + + +### Tracking Block Progress + + + +```python +from baml_client import b, events +from baml_client.types import BlockEvent + +def track_blocks(event: BlockEvent): + indent = " " * (event.block_level - 1) + action = "Starting" if event.event_type == "enter" else "Completed" + print(f"{indent}{action}: {event.block_label}") + +# Set up block tracking +ev = events.MakePosts() +events.on_block(track_blocks) + +# Run the function +posts = await b.MakePosts("https://example.com", {"events": ev}) +``` + + + +```typescript +import { b, events } from './baml-client' +import type { BlockEvent } from './baml-client/types' + +const trackBlocks = (event: BlockEvent) => { + const indent = " ".repeat(event.blockLevel - 1) + const action = event.eventType === "enter" ? "Starting" : "Completed" + console.log(`${indent}${action}: ${event.blockLabel}`) +} + +// Set up block tracking +const ev = events.MakePosts() +events.on_block(trackBlocks) + +// Run the function +const posts = await b.MakePosts("https://example.com", { events: ev }) +``` + + + +```go +func trackBlocks(event *types.BlockEvent) { + indent := strings.Repeat(" ", event.BlockLevel - 1) + action := "Starting" + if event.EventType == types.BlockEventExit { + action = "Completed" + } + fmt.Printf("%s%s: %s\n", indent, action, event.BlockLabel) +} + +func main() { + ctx := context.Background() + + // Set up block tracking + ev := events.NewMakePosts() + events.OnBlock(trackBlocks) + + // Run the function + posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{ + Events: ev, + }) + if err != nil { + log.Fatal(err) + } +} +``` + + + +## Generated Event API + + + +When you run `baml generate`, BAML analyzes your functions and creates type-safe event handlers with generic types: + +```python +# For a function with `emit let progress: int = 0` +events.on_progress(callback: (event: VarEvent[int]) -> None) + +# For a function with `emit let status: string = "starting"` +events.on_status(callback: (event: VarEvent[str]) -> None) + +# For all markdown blocks +events.on_block(callback: (event: BlockEvent) -> None) +``` + +The generic `VarEvent[T]` type provides compile-time type safety, ensuring your event handlers receive the correct data types. + + + +When you run `baml generate`, BAML analyzes your functions and creates type-safe event handlers with generic types: + +```typescript +// For a function with `emit let progress: int = 0` +events.on_progress(callback: (event: VarEvent) => void) + +// For a function with `emit let status: string = "starting"` +events.on_status(callback: (event: VarEvent) => void) + +// For all markdown blocks +events.on_block(callback: (event: BlockEvent) => void) +``` + +The generic `VarEvent` interface provides compile-time type safety, ensuring your event handlers receive the correct data types. + + + +When you run `baml generate`, BAML analyzes your functions and creates specific types for each emitted variable (since Go doesn't have user-defined generics): + +```go +// Separate types generated for each emitted variable +type ProgressVarEvent struct { + VariableName string + Value int + Timestamp time.Time + FunctionName string +} + +type StatusVarEvent struct { + VariableName string + Value string + Timestamp time.Time + FunctionName string +} + +// Corresponding callback functions +events.OnProgress(func(*types.ProgressVarEvent)) +events.OnStatus(func(*types.StatusVarEvent)) +events.OnBlock(func(*types.BlockEvent)) +``` + +Each emitted variable gets its own dedicated event type, providing the same type safety as generics while working within Go's constraints. + + + +## Best Practices + +1. **Performance**: Keep event handlers lightweight. They run sequentially in + a separate thread from the rest of the BAML runtime +1. **Error Handling**: Always include error handling in event callbacks +1. **Naming**: Use descriptive names for emitted variables to generate clear event handler names + +## Related Topics + +- [Runtime Events Guide](/guide/baml-advanced/runtime-events) - Learn how to use events in workflows +- [Collector](/ref/baml_client/collector) - Comprehensive logging system \ No newline at end of file diff --git a/fern/docs.yml b/fern/docs.yml index 24a7296ead..15d98f65b7 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -412,6 +412,9 @@ navigation: - page: Modular API icon: fa-regular fa-cubes path: 01-guide/05-baml-advanced/modular-api.mdx + - page: Runtime Events + icon: fa-regular fa-headset + path: 01-guide/05-baml-advanced/runtime-events.mdx - section: Boundary Cloud contents: # - section: Functions @@ -688,6 +691,9 @@ navigation: path: 01-guide/05-baml-advanced/client-registry.mdx - page: OnTick path: 03-reference/baml_client/ontick.mdx + - page: Runtime Events + slug: events + path: 03-reference/baml_client/runtime-events.mdx - page: Multimodal slug: media path: 03-reference/baml_client/media.mdx