From 82486ec37ec8dc0daf6db435643ad2bc2d8c3542 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 2 Sep 2025 13:40:00 -0700 Subject: [PATCH 1/6] emit docs go example --- .../05-baml-advanced/runtime-events.mdx | 295 ++++++++++++ .../baml_client/runtime-events.mdx | 444 ++++++++++++++++++ fern/docs.yml | 6 + 3 files changed, 745 insertions(+) create mode 100644 fern/01-guide/05-baml-advanced/runtime-events.mdx create mode 100644 fern/03-reference/baml_client/runtime-events.mdx 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..0fd1bd3d10 --- /dev/null +++ b/fern/01-guide/05-baml-advanced/runtime-events.mdx @@ -0,0 +1,295 @@ +--- +title: Runtime Events +--- + + +This feature was added in TODO + + +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 could 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); + } + } +} +``` + +In your client code, you can bind events to callbacks: + + + +```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() + + # 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) +``` + + +```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) + } +``` + + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/types" +) + +func main() { + ctx := context.Background() + + // Get an Events callback collector with the right type + // for your MakePosts() function. + ev := events.NewMakePosts() + + // Associate the block event with your own callback. + events.OnBlock(func(ev *types.BlockEvent) { + fmt.Println(ev.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) +} +``` + + + +## Using `emit` to track variables + +Variable update can also be tracked with events. To mark a variable as visible +to the event system, use the `emit` keyword when you declare the variable. Let’s +see how we would use this capability to track the progress of our marketing post +generation workflow: + +```tsx +function MakePosts(source_url: string) -> Post[] { + # Summarize Source + let source = LLMSummarizeSource(source_url); + + # Determine Topic + let topic = LLMInferTopic(source); + + # Generate Marketing Post Ideas + let ideas: string[] = LLMIdeas(topic, source); + + // Track how many posts we need to generate. <-*** + let posts_target_length = ideas.len(); + emit let progress_percent: int = 0; + + # Generate posts + let posts: Post[] = []; + for ((i,idea) in ideas.enumerate()) { + + ## Create the post + let post = LLMGeneratePost(idea, source); + + ## Quality control + 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 + } +} +``` + +When you generate a BAML client, the events structure for ` MakePosts` 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`. + +In your client code, you can track these emitted variables: + + + +```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.on_progress_percent(lambda percent: print(f"Progress: {percent}%")) + + # Invoke the function. + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) + print(posts) +``` + + +```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() + + // 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) +} +``` + + +```go +// main.go +package main + +import ( + "context" + "fmt" + "log" + + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/types" +) + +func main() { + ctx := context.Background() + + // Get an Events callback collector with the right type + // for your MakePosts() function. + ev := events.NewMakePosts() + + // Track the progress_percent variable updates + events.OnProgressPercent(func(percent int) { + fmt.Printf("Progress: %d%%\n", percent) + }) + + // 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) + +# Event Details + +It helps to understand the following concepts when trying to do more complex +things with Events: + + 1. **Separate Thread** To avoid interfering with the rest of your BAML code, + callbacks are run concurrently in a separate execution thread. + 2. Events are meant for local tracking. If you asign a value to a new (non- + emit) variable, this new variable doesn't get its updates tracked. If you + pass a value as a parameter to a function, updates made within that + function will not be tracked. \ No newline at end of file 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..13ee8e2630 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/events.mdx - page: Multimodal slug: media path: 03-reference/baml_client/media.mdx From 5f6d1b4fd2e6055e0128d8c97755cf7be24a6dda Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 8 Sep 2025 14:59:01 -0700 Subject: [PATCH 2/6] go channels --- .../05-baml-advanced/runtime-events.mdx | 481 ++++++++++++++++-- 1 file changed, 428 insertions(+), 53 deletions(-) diff --git a/fern/01-guide/05-baml-advanced/runtime-events.mdx b/fern/01-guide/05-baml-advanced/runtime-events.mdx index 0fd1bd3d10..caa3149432 100644 --- a/fern/01-guide/05-baml-advanced/runtime-events.mdx +++ b/fern/01-guide/05-baml-advanced/runtime-events.mdx @@ -33,20 +33,20 @@ struct Post { 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) { @@ -56,35 +56,65 @@ function MakePosts(source_url: string, count: int) -> Post[] { } ``` -In your client code, you can bind events to callbacks: +## Tracking 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. -```python + +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.MakePosts() - + 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 // 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. @@ -94,7 +124,7 @@ In your client code, you can bind events to callbacks: events.on_block((ev) => { console.log(ev.block_label) }); - + // Invoke the function. const posts = await b.MakePosts( "https://wikipedia.org/wiki/DNA", @@ -105,6 +135,9 @@ In your client code, you can bind events to callbacks: ``` + +In your client code, you can bind events to callbacks: + ```go // main.go package main @@ -124,13 +157,13 @@ func main() { // Get an Events callback collector with the right type // for your MakePosts() function. - ev := events.NewMakePosts() - - // Associate the block event with your own callback. - events.OnBlock(func(ev *types.BlockEvent) { - fmt.Println(ev.BlockLabel) + ev := events.NewMakePostsEventsCollector() + + // Register a handler for block events + ev.OnBlock(func(blockEvent events.BlockEvent) { + fmt.Println(blockEvent.BlockLabel) }) - + // Invoke the function. posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ Events: ev, @@ -144,35 +177,35 @@ func main() { -## Using `emit` to track variables +## Track variables with `emit` -Variable update can also be tracked with events. To mark a variable as visible -to the event system, use the `emit` keyword when you declare the variable. Let’s +Variable updates can also be tracked with events. To mark an update for tracking, +use the `emit` keyword on any statement that creates or updates a variable. Let’s see how we would use this capability to track the progress of our marketing post generation workflow: -```tsx +```baml BAML function MakePosts(source_url: string) -> Post[] { # Summarize Source let source = LLMSummarizeSource(source_url); - + # Determine Topic let topic = LLMInferTopic(source); - + # Generate Marketing Post Ideas let ideas: string[] = LLMIdeas(topic, source); - + // Track how many posts we need to generate. <-*** let posts_target_length = ideas.len(); emit let progress_percent: int = 0; - + # Generate posts let posts: Post[] = []; for ((i,idea) in ideas.enumerate()) { - + ## Create the post let post = LLMGeneratePost(idea, source); - + ## Quality control let quality = LLMJudgePost(post, idea, source); if (quality > 8) { @@ -180,23 +213,47 @@ function MakePosts(source_url: string) -> Post[] { } else { posts_target_length -= 1; } - + // *** This update will trigger events visible to the client. - progress_percent = i * 100 / posts_target_length + emit progress_percent = i * 100 / posts_target_length } } ``` -When you generate a BAML client, the events structure for ` MakePosts` will +When you generate a BAML client, the events structure for `MakePosts` 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`. -In your client code, you can track these emitted variables: - + +## Tracking Emitted Variables + +You can track changes to emitted variables from the client code. When you generate +client code from your BAML code, we produce event listener structs. + ```python +# baml_client/events.py + +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 MakePostsEventCollector: + """Event collector for MakePosts function""" + + def on_var_progress_percent(self, handler: Callable[[VarEvent[int]], None]) -> None: + """Register a handler for progress_percent variable updates""" + ... + # app.py from baml_client.sync_client import { b } from baml_client.types import Event @@ -206,16 +263,23 @@ 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.on_progress_percent(lambda percent: print(f"Progress: {percent}%")) - + # Invoke the function. posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) print(posts) ``` + + +In your client code, you can track these emitted variables, by constructing an +event listener specific to the workflow you are using. Then populating that +listener with your own callback functions, and finally supplying the listener +when you invoke the function. + ```typescript // index.ts import { b, events } from "./baml-client" @@ -230,17 +294,24 @@ async function Example() { events.on_progress_percent((percent) => { console.log(`Progress: ${percent}%`) }); - + // Invoke the function. const posts = await b.MakePosts( "https://wikipedia.org/wiki/DNA", - {"events": ev} + {"events": ev } ) console.log(posts) } ``` + + +In your client code, you can track these emitted variables by constructing an +event listener specific to the workflow you are using. Then populating that +listener with your own callback functions, and finally supplying the listener +when you invoke the function. + ```go // main.go package main @@ -260,13 +331,13 @@ func main() { // Get an Events callback collector with the right type // for your MakePosts() function. - ev := events.NewMakePosts() - - // Track the progress_percent variable updates - events.OnProgressPercent(func(percent int) { - fmt.Printf("Progress: %d%%\n", percent) + ev := events.NewMakePostsEventsCollector() + + // Register a handler for progress_percent variable updates + ev.OnVarProgressPercent(func(percent events.VarEvent[int]) { + fmt.Printf("Progress: %d%%\n", percent.Value) }) - + // Invoke the function. posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ Events: ev, @@ -282,14 +353,318 @@ func main() { For details about the types of events, see [BAML Language Reference](/ref/baml_client/events) -# Event Details -It helps to understand the following concepts when trying to do more complex -things with Events: +# Usage Scenarios + +## Tracking 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 `emit`. Anything causing a change to a value will +cause the value to emit an event to listeners that listen to the original +variable. + +```baml BAML +emit let x = Foo { name: "BAML" }; // Make a BAML value +let y = x; // Alias x to a new name +emit y.name = "Baml"; // Modify the new name => triggers event + +emit let a: int = 1; // Make a BAML value +let b = a; // Alias a to a new name +emit 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. + + +## Tracking 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 +emit let x = Foo { name: "BAML" }; // Make a 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. + +## Tracking 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. + + + 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, you once again use the `emit` keyword, now in the function's +parameters. + +```baml BAML +function Main() -> int { + emit let state = Foo { + name: "BAML", + counter: 0, + }; // Make a BAML value + 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 as `emit`, so `state` in `Main()` will fire two events, +// one for each update of `s`. +function ChangeState(emit s: Foo) -> null { + s.counter++; + s.name = "Baml"; +} +``` + +## Tracking a dynamic state + +In the first example on this page, we showed how to track workflow state using +block events. In some cases, you may need more control over the state you are tracking, +either because your state contains structured data, or because you are calling many +functions and need custom logic to describe where the workflow is within its process. + +Let's consider an example that both uses a custom state type and sends that state +through various workflow functions to collect status updates: + +```baml BAML +// A workflow for downloading and summarizing websites. + +// The data about our workflow that we want to send to the user. +// Enough to render a progress bar like: +// +// |--------62% | +// Downloading: wikipedia.org/wiki/owl +// Downloading: wikipedia.org/wiki/cat +// Summarizing: wikipedia.org/wiki/dog +// Summarizing: wikipedia.org/wiki/dog +// Errors: 2 +struct WorkflowState { + progressPercent int + downloading []string + summarizing []string + errors int +} + +// The return value of the workflow, a mapping from input topic +// to summary of that topic. +struct Summaries { + summaries map +} + +// A workflow to summarize a list of topics on wikipedia. +function SummarizeWikiArticles(topics: string[]) -> Summaries { + + // Our main readout of the workflow state to the client. + emit let state = WorkflowState{ + progressPercent: 0, + downloading: [], + summarizing: [], + }; + + let summaries = {}; + + for topic in topics { + let summary = DownloadAndSummarize(topic, state); + if (summary != null) { + summaries[topic] = summary; + } + + // Update the state with new progressPercent. + state.progressPercent = (summaries.len() + state.errors) * 100 / topics.len(); + } + + return summaries; +} + +function DownloadAndSummarize(topic: string, state: WorkflowState) -> string? { + state.downloading.push(topic); + let article = std.fetch(std.Request { + url: "https://en.wikipedia.org/wiki/" + topic, + method: "GET", + }); + state.downloading.pop(topic); + state.summarizing.push(topic); + let summary = LLMSummarize(article, state); + state.summarizing.pop(topic); + let result = if (summary == null) { + state.errors += 1; + null + } else { + summary + } + return result; +} + +function LLMSummarize(article: string) -> string? { + client GPT4 + prompt #" + Summarize the following article into two sentences: {{ article }} + + If there is a problem summarizing, return null. + "# +} +``` + +# Streaming + +If updates to `emit` variables include large amounts of data that you want to start +surfacing to your application before they are done being generated, you may 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 simple return their values in a single +complete chunk. + +```baml BAML +function DescribeTerminatorMovies() -> string[] { + let results = []; + for (x in [1,2,3]) { + emit let result = LLMElaborateOnTopic("Terminator " + std.to_string(x)); + results.push(result); + } + return results; +} +``` + +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. + +The streaming listeners are available in client code under the streaming +module. + + + + +# 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 emit status = "Starting analysis of " + messages.length().to_string() + " messages" + + 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) +} +``` + +## AI SDK Generators + +## Mastra `.watch()` + +# The Rules of emit + +There are times when we suppress events. - 1. **Separate Thread** To avoid interfering with the rest of your BAML code, - callbacks are run concurrently in a separate execution thread. - 2. Events are meant for local tracking. If you asign a value to a new (non- - emit) variable, this new variable doesn't get its updates tracked. If you - pass a value as a parameter to a function, updates made within that - function will not be tracked. \ No newline at end of file +Emit on object instance changes. +Emit when the id is the same but the deep equality changes. From e69541c2d28a3d339b260df8bfff15d3ca460756 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 8 Sep 2025 14:59:01 -0700 Subject: [PATCH 3/6] go channels --- .../05-baml-advanced/runtime-events.mdx | 39 +++++++++++++++++++ fern/docs.yml | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/fern/01-guide/05-baml-advanced/runtime-events.mdx b/fern/01-guide/05-baml-advanced/runtime-events.mdx index caa3149432..6adb2dfa9a 100644 --- a/fern/01-guide/05-baml-advanced/runtime-events.mdx +++ b/fern/01-guide/05-baml-advanced/runtime-events.mdx @@ -312,6 +312,45 @@ event listener specific to the workflow you are using. Then populating that listener with your own callback functions, and finally supplying the listener when you invoke the function. +```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 { + onBlockHandler func(BlockEvent) + onVarProgressPercentHandler func(VarEvent[int]) +} + +func NewMakePostsEventCollector() *MakePostsEventCollector { + return &MakePostsEventCollector{} +} + +// OnBlock registers a handler for block events +func (c *MakePostsEventCollector) OnBlock(handler func(BlockEvent)) { + c.onBlockHandler = handler +} + +// OnVarProgressPercent registers a handler for progress_percent variable updates +func (c *MakePostsEventCollector) OnVarProgressPercent(handler func(VarEvent[int])) { + c.onVarProgressPercentHandler = handler +} +``` + ```go // main.go package main diff --git a/fern/docs.yml b/fern/docs.yml index 13ee8e2630..15d98f65b7 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -693,7 +693,7 @@ navigation: path: 03-reference/baml_client/ontick.mdx - page: Runtime Events slug: events - path: 03-reference/baml_client/events.mdx + path: 03-reference/baml_client/runtime-events.mdx - page: Multimodal slug: media path: 03-reference/baml_client/media.mdx From 644af099ad4aa906e48c3ff35f4ca8146a20e37e Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 15 Sep 2025 11:50:06 -0700 Subject: [PATCH 4/6] update emit docs --- .../05-baml-advanced/runtime-events.mdx | 114 +++++++++++++++--- 1 file changed, 94 insertions(+), 20 deletions(-) diff --git a/fern/01-guide/05-baml-advanced/runtime-events.mdx b/fern/01-guide/05-baml-advanced/runtime-events.mdx index 6adb2dfa9a..da3ac5a8be 100644 --- a/fern/01-guide/05-baml-advanced/runtime-events.mdx +++ b/fern/01-guide/05-baml-advanced/runtime-events.mdx @@ -3,7 +3,6 @@ title: Runtime Events --- -This feature was added in TODO When running multi-step workflows, you need to be able to get information about @@ -19,7 +18,7 @@ invoke the workflow. 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 could directly use the markdown headers to +the following example, your client can directly use the markdown headers to render the current status on a status page: ```baml BAML @@ -110,6 +109,19 @@ class MakePostsEventCollector: 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" @@ -138,6 +150,21 @@ In your client code, you can bind events to callbacks: In your client code, you can bind events to callbacks: +```go +// baml_client/events.go +package events + +type BlockEvent struct { + BlockLabel string `json:"block_label"` + EventType string `json:"event_type"` // "enter" | "exit" +} + +type MakePostsEventsCollector struct { + blockEvents chan BlockEvent + // ... there are other fields we will describe later. +} +``` + ```go // main.go package main @@ -220,19 +247,18 @@ function MakePosts(source_url: string) -> Post[] { } ``` -When you generate a BAML client, the events structure for `MakePosts` 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`. +## Tracking Emitted Variables + -## Tracking Emitted Variables + -You can track changes to emitted variables from the client code. When you generate -client code from your BAML code, we produce event listener structs. +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`. - ```python # baml_client/events.py @@ -247,13 +273,15 @@ class VarEvent(Generic[T]): timestamp: str function_name: str +class MakePostsVarsCollector: + progress_percent: Callable[[VarEvent[int]], None] + class MakePostsEventCollector: """Event collector for MakePosts function""" + vars: MakePostsVarsCollector +``` - def on_var_progress_percent(self, handler: Callable[[VarEvent[int]], None]) -> None: - """Register a handler for progress_percent variable updates""" - ... - +```python # app.py from baml_client.sync_client import { b } from baml_client.types import Event @@ -265,7 +293,7 @@ def Example(): ev = events.MakePosts() # Track the progress_percent variable updates - events.on_progress_percent(lambda percent: print(f"Progress: {percent}%")) + events.vars.on_progress_percent(lambda percent: print(f"Progress: {percent}%")) # Invoke the function. posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) @@ -275,15 +303,32 @@ def Example(): -In your client code, you can track these emitted variables, by constructing an -event listener specific to the workflow you are using. Then populating that -listener with your own callback functions, and finally supplying the listener -when you invoke the function. +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 { Event } from "./baml-client/types" +import type { VarEvent } from "./baml-client/types" async function Example() { // Get an Events callback collector with the right type @@ -351,6 +396,35 @@ func (c *MakePostsEventCollector) OnVarProgressPercent(handler func(VarEvent[int } ``` +```go +// baml_client/events.go +package events + +import "time" + +type VarEvent[T any] struct { + VariableName string `json:"variable_name"` + Value T `json:"value"` +} + +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), + } +} + +// ProgressPercentEvents returns a channel for receiving progress_percent variable updates +func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] { + return c.progressPercentEvents +} +``` + ```go // main.go package main From d3a9a318052c1e14104003c207e6824368e5c580 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Mon, 15 Sep 2025 11:50:06 -0700 Subject: [PATCH 5/6] xJJ: Enter a description for the combined commit. update version number for emit docs Update to auto emit and dollar syntax --- .../05-baml-advanced/runtime-events.mdx | 783 --------- .../runtime-observability.mdx | 1447 +++++++++++++++++ result | 1 + 3 files changed, 1448 insertions(+), 783 deletions(-) delete mode 100644 fern/01-guide/05-baml-advanced/runtime-events.mdx create mode 100644 fern/01-guide/05-baml-advanced/runtime-observability.mdx create mode 120000 result diff --git a/fern/01-guide/05-baml-advanced/runtime-events.mdx b/fern/01-guide/05-baml-advanced/runtime-events.mdx deleted file mode 100644 index da3ac5a8be..0000000000 --- a/fern/01-guide/05-baml-advanced/runtime-events.mdx +++ /dev/null @@ -1,783 +0,0 @@ ---- -title: Runtime Events ---- - - - - -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); - } - } -} -``` - -## Tracking 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 bind events to callbacks: - -```go -// baml_client/events.go -package events - -type BlockEvent struct { - BlockLabel string `json:"block_label"` - EventType string `json:"event_type"` // "enter" | "exit" -} - -type MakePostsEventsCollector struct { - blockEvents chan BlockEvent - // ... there are other fields we will describe later. -} -``` - -```go -// main.go -package main - -import ( - "context" - "fmt" - "log" - - b "example.com/myproject/baml_client" - "example.com/myproject/baml_client/events" - "example.com/myproject/baml_client/types" -) - -func main() { - ctx := context.Background() - - // Get an Events callback collector with the right type - // for your MakePosts() function. - ev := events.NewMakePostsEventsCollector() - - // Register a handler for block events - ev.OnBlock(func(blockEvent events.BlockEvent) { - 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, -use the `emit` keyword on any statement that creates or updates a variable. Let’s -see how we would use this capability to track the progress of our marketing post -generation workflow: - -```baml BAML -function MakePosts(source_url: string) -> Post[] { - # Summarize Source - let source = LLMSummarizeSource(source_url); - - # Determine Topic - let topic = LLMInferTopic(source); - - # Generate Marketing Post Ideas - let ideas: string[] = LLMIdeas(topic, source); - - // Track how many posts we need to generate. <-*** - let posts_target_length = ideas.len(); - emit let progress_percent: int = 0; - - # Generate posts - let posts: Post[] = []; - for ((i,idea) in ideas.enumerate()) { - - ## Create the post - let post = LLMGeneratePost(idea, source); - - ## Quality control - 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. - emit progress_percent = i * 100 / posts_target_length - } -} -``` - -## Tracking Emitted Variables - - - - - - -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`. - -```python -# baml_client/events.py - -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 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 an -event listener specific to the workflow you are using. Then populating that -listener with your own callback functions, and finally supplying the listener -when you invoke the function. - -```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 { - onBlockHandler func(BlockEvent) - onVarProgressPercentHandler func(VarEvent[int]) -} - -func NewMakePostsEventCollector() *MakePostsEventCollector { - return &MakePostsEventCollector{} -} - -// OnBlock registers a handler for block events -func (c *MakePostsEventCollector) OnBlock(handler func(BlockEvent)) { - c.onBlockHandler = handler -} - -// OnVarProgressPercent registers a handler for progress_percent variable updates -func (c *MakePostsEventCollector) OnVarProgressPercent(handler func(VarEvent[int])) { - c.onVarProgressPercentHandler = handler -} -``` - -```go -// baml_client/events.go -package events - -import "time" - -type VarEvent[T any] struct { - VariableName string `json:"variable_name"` - Value T `json:"value"` -} - -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), - } -} - -// ProgressPercentEvents returns a channel for receiving 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" - "example.com/myproject/baml_client/types" -) - -func main() { - ctx := context.Background() - - // Get an Events callback collector with the right type - // for your MakePosts() function. - ev := events.NewMakePostsEventsCollector() - - // Register a handler for progress_percent variable updates - ev.OnVarProgressPercent(func(percent events.VarEvent[int]) { - 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) - - -# Usage Scenarios - -## Tracking 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 `emit`. Anything causing a change to a value will -cause the value to emit an event to listeners that listen to the original -variable. - -```baml BAML -emit let x = Foo { name: "BAML" }; // Make a BAML value -let y = x; // Alias x to a new name -emit y.name = "Baml"; // Modify the new name => triggers event - -emit let a: int = 1; // Make a BAML value -let b = a; // Alias a to a new name -emit 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. - - -## Tracking 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 -emit let x = Foo { name: "BAML" }; // Make a 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. - -## Tracking 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. - - - 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, you once again use the `emit` keyword, now in the function's -parameters. - -```baml BAML -function Main() -> int { - emit let state = Foo { - name: "BAML", - counter: 0, - }; // Make a BAML value - 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 as `emit`, so `state` in `Main()` will fire two events, -// one for each update of `s`. -function ChangeState(emit s: Foo) -> null { - s.counter++; - s.name = "Baml"; -} -``` - -## Tracking a dynamic state - -In the first example on this page, we showed how to track workflow state using -block events. In some cases, you may need more control over the state you are tracking, -either because your state contains structured data, or because you are calling many -functions and need custom logic to describe where the workflow is within its process. - -Let's consider an example that both uses a custom state type and sends that state -through various workflow functions to collect status updates: - -```baml BAML -// A workflow for downloading and summarizing websites. - -// The data about our workflow that we want to send to the user. -// Enough to render a progress bar like: -// -// |--------62% | -// Downloading: wikipedia.org/wiki/owl -// Downloading: wikipedia.org/wiki/cat -// Summarizing: wikipedia.org/wiki/dog -// Summarizing: wikipedia.org/wiki/dog -// Errors: 2 -struct WorkflowState { - progressPercent int - downloading []string - summarizing []string - errors int -} - -// The return value of the workflow, a mapping from input topic -// to summary of that topic. -struct Summaries { - summaries map -} - -// A workflow to summarize a list of topics on wikipedia. -function SummarizeWikiArticles(topics: string[]) -> Summaries { - - // Our main readout of the workflow state to the client. - emit let state = WorkflowState{ - progressPercent: 0, - downloading: [], - summarizing: [], - }; - - let summaries = {}; - - for topic in topics { - let summary = DownloadAndSummarize(topic, state); - if (summary != null) { - summaries[topic] = summary; - } - - // Update the state with new progressPercent. - state.progressPercent = (summaries.len() + state.errors) * 100 / topics.len(); - } - - return summaries; -} - -function DownloadAndSummarize(topic: string, state: WorkflowState) -> string? { - state.downloading.push(topic); - let article = std.fetch(std.Request { - url: "https://en.wikipedia.org/wiki/" + topic, - method: "GET", - }); - state.downloading.pop(topic); - state.summarizing.push(topic); - let summary = LLMSummarize(article, state); - state.summarizing.pop(topic); - let result = if (summary == null) { - state.errors += 1; - null - } else { - summary - } - return result; -} - -function LLMSummarize(article: string) -> string? { - client GPT4 - prompt #" - Summarize the following article into two sentences: {{ article }} - - If there is a problem summarizing, return null. - "# -} -``` - -# Streaming - -If updates to `emit` variables include large amounts of data that you want to start -surfacing to your application before they are done being generated, you may 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 simple return their values in a single -complete chunk. - -```baml BAML -function DescribeTerminatorMovies() -> string[] { - let results = []; - for (x in [1,2,3]) { - emit let result = LLMElaborateOnTopic("Terminator " + std.to_string(x)); - results.push(result); - } - return results; -} -``` - -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. - -The streaming listeners are available in client code under the streaming -module. - - - - -# 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 emit status = "Starting analysis of " + messages.length().to_string() + " messages" - - 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) -} -``` - -## AI SDK Generators - -## Mastra `.watch()` - -# The Rules of emit - -There are times when we suppress events. - -Emit on object instance changes. -Emit when the id is the same but the deep equality changes. diff --git a/fern/01-guide/05-baml-advanced/runtime-observability.mdx b/fern/01-guide/05-baml-advanced/runtime-observability.mdx new file mode 100644 index 0000000000..ed927bf023 --- /dev/null +++ b/fern/01-guide/05-baml-advanced/runtime-observability.mdx @@ -0,0 +1,1447 @@ +--- +title: Runtime Observability +--- + + +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 Block Notifications + +You can watch block notifications from your client code. + +When you generate client code from your BAML code, we produce watcher structs +that allow you to hook in to its execution. + + + + +In your client code, you can bind events to callbacks: + +```python Python + # baml_client/watchers.py +from typing import TypeVar, Generic, Callable, Union + +class BlockNotification: + """ + Notification fired when entering or exiting a markdown block + """ + block_label: str + event_type: str # "enter" | "exit" + +class MakePosts: + """Watcher for MakePosts function""" + + def on_block(self, handler: Callable[[BlockNotification], None]) -> None: + """Register a handler for block notification""" + pass +``` + +```python Python + # app.py + from baml_client.sync_client import { b } + from baml_client.types import Notification + import baml_client.watchers + + def Example(): + # Get a watcher with the right type for your MakePosts() function. + watcher = watchers.MakePosts() + + # Associate the block event with your own callback. + watcher.on_block(lambda notif: print(notif.block_label)) + + # Invoke the function. + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": ev}) + print(posts) +``` + + + +In your client code, you can bind events to callbacks: + +```typescript +// baml_client/event.ts +export interface BlockNotification { + 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/result b/result new file mode 120000 index 0000000000..8693364833 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/l1342ji44qwx7xx1lsdk7z8qrh9yz1ax-baml-cli-0.211.0 \ No newline at end of file From ee94bed25c075b749a932c1bcc24dcf908b31e86 Mon Sep 17 00:00:00 2001 From: Greg Hale Date: Tue, 14 Oct 2025 11:31:04 -0700 Subject: [PATCH 6/6] rename emit to watch and event to notification --- .gitignore | 1 + .../runtime-observability.mdx | 692 +++++++++--------- .../baml_client/runtime-events.mdx | 444 ----------- .../baml_client/runtime-observability.mdx | 459 ++++++++++++ fern/docs.yml | 10 +- result | 1 - 6 files changed, 811 insertions(+), 796 deletions(-) delete mode 100644 fern/03-reference/baml_client/runtime-events.mdx create mode 100644 fern/03-reference/baml_client/runtime-observability.mdx delete mode 120000 result diff --git a/.gitignore b/.gitignore index 6cae97504c..c4e17613b7 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,4 @@ jj-workflow.mdc third_party baml_repl_history.txt +trace_events_debug.json diff --git a/fern/01-guide/05-baml-advanced/runtime-observability.mdx b/fern/01-guide/05-baml-advanced/runtime-observability.mdx index ed927bf023..9a0b3ffec8 100644 --- a/fern/01-guide/05-baml-advanced/runtime-observability.mdx +++ b/fern/01-guide/05-baml-advanced/runtime-observability.mdx @@ -11,7 +11,7 @@ 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 makes this possible though a notification system that connects variables in your BAML Workflow code to the Python/TypeScript/etc client code that you used to invoke the workflow. @@ -60,13 +60,13 @@ function MakePosts(source_url: string, count: int) -> Post[] { You can watch block notifications from your client code. -When you generate client code from your BAML code, we produce watcher structs +When you generate client code from your BAML code, we produce watchers that allow you to hook in to its execution. -In your client code, you can bind events to callbacks: +In your client code, you can bind notifications to callbacks: ```python Python # baml_client/watchers.py @@ -97,50 +97,50 @@ class MakePosts: # Get a watcher with the right type for your MakePosts() function. watcher = watchers.MakePosts() - # Associate the block event with your own callback. + # Associate the block notification with your own callback. watcher.on_block(lambda notif: print(notif.block_label)) # Invoke the function. - posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": ev}) + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher}) print(posts) ``` -In your client code, you can bind events to callbacks: +In your client code, you can bind notifications to callbacks: ```typescript -// baml_client/event.ts +// baml_client/watchers.ts export interface BlockNotification { block_label: string; event_type: "enter" | "exit"; } -export interface MakePostsEventCollector { - // Register a handler for block events. - on_block(handler: (ev: BlockEvent) => void): void; +export interface MakePosts { + // Register a handler for block notifications. + on_block(handler: (notif: BlockNotification) => void): void; } ``` ```typescript // index.ts - import { b, events } from "./baml-client" - import type { Event } from "./baml-client/types" + import { b, watchers } from "./baml-client" + import type { Notification } from "./baml-client/types" async function Example() { - // Get an Events callback collector with the right type + // Get a watcher with the right type // for your MakePosts() function. - let ev = events.MakePosts() + let watcher = watchers.MakePosts() - // Associate the block event with your own callback. - events.on_block((ev) => { - console.log(ev.block_label) + // Associate the block notification with your own callback. + watcher.on_block((notif) => { + console.log(notif.block_label) }); // Invoke the function. const posts = await b.MakePosts( "https://wikipedia.org/wiki/DNA", - {"events": ev} + {"watchers": watcher} ) console.log(posts) } @@ -148,31 +148,31 @@ export interface MakePostsEventCollector { -In your client code, you can consume events via channels: +In your client code, you can consume notifications via channels: ```go -// baml_client/events.go -package events +// baml_client/watchers.go +package watchers -type BlockEvent struct { +type BlockNotification 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. +type MakePosts struct { + blockNotifications chan BlockNotification + // ... additional notification channels are initialized elsewhere. } -func NewMakePostsEventCollector() *MakePostsEventCollector { - return &MakePostsEventCollector{ - blockEvents: make(chan BlockEvent, 100), +func NewMakePosts() *MakePosts { + return &MakePosts{ + blockNotifications: make(chan BlockNotification, 100), } } -// BlockEvents provides block execution updates as a channel. -func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent { - return c.blockEvents +// BlockNotifications provides block execution updates as a channel. +func (c *MakePosts) BlockNotifications() <-chan BlockNotification { + return c.blockNotifications } ``` @@ -186,26 +186,26 @@ import ( "log" b "example.com/myproject/baml_client" - "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/watchers" ) func main() { ctx := context.Background() - // Get an event collector with the right channels + // Get a watcher with the right channels // for your MakePosts() function. - ev := events.NewMakePostsEventCollector() + watcher := watchers.NewMakePosts() - // Consume block events asynchronously so updates are printed as they arrive. + // Consume block notifications asynchronously so updates are printed as they arrive. go func() { - for blockEvent := range ev.BlockEvents() { - fmt.Println(blockEvent.BlockLabel) + for blockNotif := range watcher.BlockNotifications() { + fmt.Println(blockNotif.BlockLabel) } }() // Invoke the function. posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ - Events: ev, + Watchers: watcher, }) if err != nil { log.Fatal(err) @@ -216,36 +216,36 @@ func main() { -## Track variables with `emit` +## Track variables with `@watch` -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. +Variable updates can also be tracked with notifications. To mark an update for tracking, +attach `@watch` 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 +let foo = State { counter: 0 } @watch; +foo.counter += 1; // *** This triggers a notification ``` ```python - events.on("foo", lambda st: my_state_update_handler(st)) + watcher.vars.foo(lambda st: my_state_update_handler(st)) ``` ```typescript - events.on("foo", (st) => my_state_update_handler(st)) + watcher.on_var_foo((st) => my_state_update_handler(st)) ``` ```go - // Consume events from the events.on_foo channel - for st := range events.FooEvents { + // Consume notifications from the watcher.FooNotifications channel + for st := range watcher.FooNotifications() { 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. +Updates can be tracked automatically or manually, depending on the `@watch` options you choose. +Automatic tracking will emit notifications any time a variable is updated. Manual tracking +only emits notifications after updates that you specify. ### Auto Tracking @@ -262,7 +262,7 @@ function MakePosts(source_url: string) -> Post[] { let ideas: string[] = LLMIdeas(topic, source); let posts_target_length = ideas.len(); - let progress_percent: int = 0 @emit; // *** Emit marker used here. + let progress_percent: int = 0 @watch; // *** Watch marker used here. let posts: Post[] = []; for ((i,idea) in ideas.enumerate()) { @@ -273,28 +273,28 @@ function MakePosts(source_url: string) -> Post[] { } else { posts_target_length -= 1; } - // *** This update will trigger events visible to the client. + // *** This update will trigger notifications visible to the client. progress_percent = i * 100 / posts_target_length } } ``` -### Emit parameters +### Watch 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) + - `@watch(when=MyFilterFunc)` - Only emits when `MyFilterFunc` returns `true` + - `@watch(when=manual)` - Never auto emit (only emit when manually triggered) + - `@watch(skip_def=true)` - Emits every time the variable is updated, but not on initialization + - `@watch(name=my_channel)` - Emits notifications 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 +second is the new version. With these two parameters, you can determine whether the notification 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` +If you do not specify a filter function, BAML deduplicates automatically emitted notifications for you. +You could replicate the same behavior by using `@watch(when=MyFilterFunc)` where `MyFilterFunc` is defined as: ```baml BAML @@ -307,30 +307,30 @@ function MyFilterFunc(prev: MyObject, curr: MyObject) -> bool { 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. +under construction. In that case, use `@watch(when=manual)` when initializing the variable, and +call `.watchers.$notify()` on the variable when you want to manually trigger a notification. ```baml BAML function ConstructValue(description: string) -> Character { - let character = Character { name: "", age: 0, skills: [] } @emit(when=false); + let character = Character { name: "", age: 0, skills: [] } @watch(when=manual); character.name = LLMChooseName(description); character.age = LLMChooseAge(description); character.skills = LLMChooseSkills(description); - character.$emit() // *** Only emit when done building the character. + character.watchers.$notify() // *** Only notify when done building the character. } ``` ### Sharing a Channel -Sometimes you want multiple variables to send update events on the same channel, for example, +Sometimes you want multiple variables to send update notifications 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); + let status = "Starting" @watch(name=updates); + let progress = 0 @watch(name=updates, skip_def=true); for (let i = 0; i < 100; i++) { progress = i; // *** These updates will apear on the `updates` channel. } @@ -339,75 +339,75 @@ function DoWork() -> bool { } ``` -## Receiving Events from Client Code +## Receiving Notifications 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 +include a `MakePosts` watcher 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 we marked that variable with `@watch`. The callbacks will receive an `int` data payload, because `progress_percent` is an `int`. ```python -# baml_client/events.py +# baml_client/watchers.py T = TypeVar('T') -class VarEvent(Generic[T]): +class VarNotification(Generic[T]): """ - Event fired when an emitted variable is updated + Notification fired when a watched variable is updated """ value: T timestamp: str function_name: str class MakePostsVarsCollector: - progress_percent: Callable[[VarEvent[int]], None] + progress_percent: Callable[[VarNotification[int]], None] -class MakePostsEventCollector: - """Event collector for MakePosts function""" +class MakePosts: + """Watcher 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 +from baml_client.types import Notification +import baml_client.watchers def Example(): - # Get an Events callback collector with the right type + # Get a watcher with the right type # for your MakePosts() function. - ev = events.MakePosts() + watcher = watchers.MakePosts() # Track the progress_percent variable updates - events.vars.on_progress_percent(lambda percent: print(f"Progress: {percent}%")) + watcher.vars.progress_percent(lambda percent: print(f"Progress: {percent}%")) # Invoke the function. - posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"events": ev}) + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher}) print(posts) ``` -When you generate a BAML client for this function, its `MakePostsEventCollector` +When you generate a BAML client for this function, its `MakePosts` watcher will accept callbacks for `progress_percent` because we marked that variable with -`@emit`, and the callbacks will receive an `int` data payload, because +`@watch`, and the callbacks will receive an `int` data payload, because `progress_percent` is an `int`. ```typescript -// baml_client/events.ts -import { VarEvent } from "./types" +// baml_client/watchers.ts +import { VarNotification } from "./types" -export interface MakePostsEventCollector { +export interface MakePosts { on_var_progress_percent(callback: (percent: number) => void): void } -export function MakePosts(): MakePostsEventCollector { +export function MakePosts(): MakePosts { return { on_var_progress_percent(callback: (percent: number) => void): void { // Implementation details @@ -418,23 +418,23 @@ export function MakePosts(): MakePostsEventCollector { ```typescript // index.ts -import { b, events } from "./baml-client" -import type { VarEvent } from "./baml-client/types" +import { b, watchers } from "./baml-client" +import type { VarNotification } from "./baml-client/types" async function Example() { - // Get an Events callback collector with the right type + // Get a watcher with the right type // for your MakePosts() function. - let ev = events.MakePosts() + let watcher = watchers.MakePosts() // Track the progress_percent variable updates - events.on_progress_percent((percent) => { + watcher.on_var_progress_percent((percent) => { console.log(`Progress: ${percent}%`) }); // Invoke the function. const posts = await b.MakePosts( "https://wikipedia.org/wiki/DNA", - {"events": ev } + {"watchers": watcher } ) console.log(posts) } @@ -443,48 +443,48 @@ async function Example() { -In your client code, you can track these emitted variables by constructing the -generated event collector and reading from the channels it exposes. +In your client code, you can track these watched variables by constructing the +generated watcher and reading from the channels it exposes. ```go -// baml_client/events.go -package events +// baml_client/watchers.go +package watchers import "time" -type BlockEvent struct { +type BlockNotification struct { BlockLabel string `json:"block_label"` EventType string `json:"event_type"` // "enter" | "exit" Timestamp time.Time `json:"timestamp"` } -type VarEvent[T any] struct { +type VarNotification[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] +type MakePosts struct { + blockNotifications chan BlockNotification + progressPercentNotifications chan VarNotification[int] } -func NewMakePostsEventCollector() *MakePostsEventCollector { - return &MakePostsEventCollector{ - blockEvents: make(chan BlockEvent, 100), - progressPercentEvents: make(chan VarEvent[int], 100), +func NewMakePosts() *MakePosts { + return &MakePosts{ + blockNotifications: make(chan BlockNotification, 100), + progressPercentNotifications: make(chan VarNotification[int], 100), } } -// BlockEvents returns block execution updates. -func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent { - return c.blockEvents +// BlockNotifications returns block execution updates. +func (c *MakePosts) BlockNotifications() <-chan BlockNotification { + return c.blockNotifications } -// ProgressPercentEvents streams progress_percent variable updates. -func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] { - return c.progressPercentEvents +// ProgressPercentNotifications streams progress_percent variable updates. +func (c *MakePosts) ProgressPercentNotifications() <-chan VarNotification[int] { + return c.progressPercentNotifications } ``` @@ -498,32 +498,32 @@ import ( "log" b "example.com/myproject/baml_client" - "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/watchers" ) func main() { ctx := context.Background() - // Get an event collector with the right channels + // Get a watcher with the right channels // for your MakePosts() function. - ev := events.NewMakePostsEventCollector() + watcher := watchers.NewMakePosts() - // Consume block events and progress updates concurrently. + // Consume block notifications and progress updates concurrently. go func() { - for block := range ev.BlockEvents() { + for block := range watcher.BlockNotifications() { fmt.Printf("Block: %s\n", block.BlockLabel) } }() go func() { - for percent := range ev.ProgressPercentEvents() { + for percent := range watcher.ProgressPercentNotifications() { fmt.Printf("Progress: %d%%\n", percent.Value) } }() // Invoke the function. posts, err := b.MakePosts(ctx, "https://wikipedia.org/wiki/DNA", &b.MakePostsOptions{ - Events: ev, + Watchers: watcher, }) if err != nil { log.Fatal(err) @@ -534,22 +534,22 @@ func main() { -For details about the types of events, see [BAML Language Reference](/ref/baml_client/events) +For details about the types of notifications, see [BAML Language Reference](/ref/baml_client/watchers) # Streaming -If updates to variables tagged with `@emit` include large amounts of data that you want to start +If updates to variables tagged with `@watch` 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, +the streaming notification interface. Streaming notifications are available for all `@watch` 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 +an LLM function. All other streamed notifications 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; + let movie_text = LLMElaborateOnTopic("Terminator " + std.to_string(x)) @watch; results.push(movie_text); } return results; @@ -570,49 +570,49 @@ 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. +module that mirrors the structure of the regular watchers. ```python -# baml_client/events.py +# baml_client/watchers.py from typing import TypeVar, Generic, Callable -from baml_client.types import BamlStream, VarEvent +from baml_client.types import BamlStream, VarNotification T = TypeVar('T') -class VarEvent(Generic[T]): +class VarNotification(Generic[T]): """ - Event fired when an emitted variable is updated + Notification fired when a watched variable is updated """ variable_name: str value: T timestamp: str function_name: str -class BlockEvent: +class BlockNotification: """ - Event fired when entering or exiting a markdown block + Notification fired when entering or exiting a markdown block """ block_label: str event_type: str # "enter" | "exit" class MakePostsVarsCollector: - progress_percent: Callable[[VarEvent[int]], None] + progress_percent: Callable[[VarNotification[int]], None] -class DescribeTerminatorMoviesEventCollector: - """Event collector for DescribeTerminatorMovies function with both regular and streaming events""" +class DescribeTerminatorMovies: + """Watcher for DescribeTerminatorMovies function with both regular and streaming notifications""" - def on_block(self, handler: Callable[[BlockEvent], None]) -> None: - """Register a handler for block events""" + def on_block(self, handler: Callable[[BlockNotification], None]) -> None: + """Register a handler for block notifications""" pass - def on_var_movie_text(self, handler: Callable[[VarEvent[str]], None]) -> None: + def on_var_movie_text(self, handler: Callable[[VarNotification[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: + def on_stream_movie_text(self, handler: Callable[[BamlStream[VarNotification[str]]], None]) -> None: """Register a handler for streaming movie_text variable updates""" pass ``` @@ -620,21 +620,21 @@ class DescribeTerminatorMoviesEventCollector: ```python # app.py from baml_client.sync_client import b -import baml_client.events as events +import baml_client.watchers as watchers def example(): - # Create the unified event collector - ev = events.DescribeTerminatorMoviesEventCollector() + # Create the unified watcher + watcher = watchers.DescribeTerminatorMovies() - # Track streaming updates for the main emitted variable + # Track streaming updates for the main watched variable def handle_movie_text_stream(stream): - for event in stream: - print(f"Streaming movie text: {event.value}") + for notif in stream: + print(f"Streaming movie text: {notif.value}") - ev.on_stream_movie_text(handle_movie_text_stream) + watcher.on_stream_movie_text(handle_movie_text_stream) - # Invoke the function with events - results = b.DescribeTerminatorMovies({"events": ev}) + # Invoke the function with watchers + results = b.DescribeTerminatorMovies({"watchers": watcher}) print("Final results:", results) ``` @@ -642,39 +642,39 @@ def example(): ```typescript -// baml_client/events.ts -import { BamlStream, VarEvent } from "./types"; +// baml_client/watchers.ts +import { BamlStream, VarNotification } from "./types"; -export interface BlockEvent { +export interface BlockNotification { block_label: string; event_type: "enter" | "exit"; } -export interface VarEvent { +export interface VarNotification { 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; +export interface DescribeTerminatorMovies { + // Regular notification handlers + on_block(handler: (notif: BlockNotification) => void): void; + on_var_movie_text(handler: (notif: VarNotification) => void): void; - // Streaming event handlers - on_stream_movie_text(handler: (stream: BamlStream>) => void): void; + // Streaming notification handlers + on_stream_movie_text(handler: (stream: BamlStream>) => void): void; } -export function DescribeTerminatorMovies(): DescribeTerminatorMoviesEventCollector { +export function DescribeTerminatorMovies(): DescribeTerminatorMovies { return { - on_block(handler: (ev: BlockEvent) => void): void { + on_block(handler: (notif: BlockNotification) => void): void { // Implementation details }, - on_var_movie_text(handler: (ev: VarEvent) => void): void { + on_var_movie_text(handler: (notif: VarNotification) => void): void { // Implementation details }, - on_stream_movie_text(handler: (stream: BamlStream>) => void): void { + on_stream_movie_text(handler: (stream: BamlStream>) => void): void { // Implementation details } } @@ -683,21 +683,21 @@ export function DescribeTerminatorMovies(): DescribeTerminatorMoviesEventCollect ```typescript // index.ts -import { b, events } from "./baml-client" +import { b, watchers } from "./baml-client" async function example() { - // Create the unified event collector - let ev = events.DescribeTerminatorMovies() + // Create the unified watcher + let watcher = watchers.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}`) + // Track streaming updates for the main watched variable + watcher.on_stream_movie_text(async (stream) => { + for await (const notif of stream) { + console.log(`Streaming movie text: ${notif.value}`) } }) - // Invoke the function with events - const results = await b.DescribeTerminatorMovies({"events": ev}) + // Invoke the function with watchers + const results = await b.DescribeTerminatorMovies({"watchers": watcher}) console.log("Final results:", results) } ``` @@ -706,48 +706,48 @@ async function example() { ```go -// baml_client/events.go -package events +// baml_client/watchers.go +package watchers import "time" -type BlockEvent struct { +type BlockNotification struct { BlockLabel string `json:"block_label"` EventType string `json:"event_type"` // "enter" | "exit" Timestamp time.Time `json:"timestamp"` } -type VarEvent[T any] struct { +type VarNotification[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]) +type DescribeTerminatorMovies struct { + blockNotifications chan BlockNotification + movieTextNotifications chan VarNotification[string] + movieTextStreams chan (<-chan VarNotification[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 NewDescribeTerminatorMovies() *DescribeTerminatorMovies { + return &DescribeTerminatorMovies{ + blockNotifications: make(chan BlockNotification, 100), + movieTextNotifications: make(chan VarNotification[string], 100), + movieTextStreams: make(chan (<-chan VarNotification[string]), 10), } } -func (c *DescribeTerminatorMoviesEventCollector) BlockEvents() <-chan BlockEvent { - return c.blockEvents +func (c *DescribeTerminatorMovies) BlockNotifications() <-chan BlockNotification { + return c.blockNotifications } -func (c *DescribeTerminatorMoviesEventCollector) MovieTextEvents() <-chan VarEvent[string] { - return c.movieTextEvents +func (c *DescribeTerminatorMovies) MovieTextNotifications() <-chan VarNotification[string] { + return c.movieTextNotifications } -// MovieTextStreams produces a stream-of-streams for emitted movie_text updates. -func (c *DescribeTerminatorMoviesEventCollector) MovieTextStreams() <-chan (<-chan VarEvent[string]) { +// MovieTextStreams produces a stream-of-streams for watched movie_text updates. +func (c *DescribeTerminatorMovies) MovieTextStreams() <-chan (<-chan VarNotification[string]) { return c.movieTextStreams } ``` @@ -762,42 +762,42 @@ import ( "log" b "example.com/myproject/baml_client" - "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/watchers" ) func main() { ctx := context.Background() - // Create the unified event collector - ev := events.NewDescribeTerminatorMoviesEventCollector() + // Create the unified watcher + watcher := watchers.NewDescribeTerminatorMovies() - // Track block events and single-value updates concurrently. + // Track block notifications and single-value updates concurrently. go func() { - for block := range ev.BlockEvents() { + for block := range watcher.BlockNotifications() { fmt.Printf("Block: %s\n", block.BlockLabel) } }() go func() { - for movieText := range ev.MovieTextEvents() { + for movieText := range watcher.MovieTextNotifications() { 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) + for stream := range watcher.MovieTextStreams() { + go func(inner <-chan watchers.VarNotification[string]) { + for notif := range inner { + fmt.Printf("Streaming movie text: %s\n", notif.Value) } }(stream) } }() - // Invoke the function with events + // Invoke the function with watchers results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{ - Events: ev, + Watchers: watcher, }) if err != nil { log.Fatal(err) @@ -808,59 +808,59 @@ func main() { -## Combining Regular Events and Streaming +## Combining Regular Notifications and Streaming -You can use both regular events and streaming events together in a single unified collector to get comprehensive observability: +You can use both regular notifications and streaming notifications together in a single unified watcher to get comprehensive observability: ```python from baml_client.sync_client import b -import baml_client.events as events +import baml_client.watchers as watchers def comprehensive_example(): - # Create unified event collector - ev = events.DescribeTerminatorMoviesEventCollector() + # Create unified watcher + watcher = watchers.DescribeTerminatorMovies() - # 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}")) + # Regular notifications for workflow progress + watcher.on_block(lambda block: print(f"Block: {block.block_label}")) + watcher.vars.movie_text(lambda movie_text: print(f"Variable movie text: {movie_text.value}")) - # Streaming events for real-time content + # Streaming notifications for real-time content def handle_stream(stream): - for event in stream: - print(f"Streaming content: {event.value}") + for notif in stream: + print(f"Streaming content: {notif.value}") - ev.on_stream_movie_text(handle_stream) + watcher.on_stream_movie_text(handle_stream) - # Use single events parameter - results = b.DescribeTerminatorMovies({"events": ev}) + # Use single watchers parameter + results = b.DescribeTerminatorMovies({"watchers": watcher}) ``` ```typescript -import { b, events } from "./baml-client" +import { b, watchers } from "./baml-client" async function comprehensiveExample() { - // Create unified event collector - let ev = events.DescribeTerminatorMovies() + // Create unified watcher + let watcher = watchers.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}`)) + // Regular notifications for workflow progress + watcher.on_block((block) => console.log(`Block: ${block.block_label}`)) + watcher.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}`) + // Streaming notifications for real-time content + watcher.on_stream_movie_text(async (stream) => { + for await (const notif of stream) { + console.log(`Streaming content: ${notif.value}`) } }) - // Use single events parameter - const results = await b.DescribeTerminatorMovies({ events: ev }) + // Use single watchers parameter + const results = await b.DescribeTerminatorMovies({ watchers: watcher }) } ``` @@ -871,35 +871,35 @@ async function comprehensiveExample() { func comprehensiveExample() { ctx := context.Background() - // Create unified event collector - ev := events.NewDescribeTerminatorMoviesEventCollector() + // Create unified watcher + watcher := watchers.NewDescribeTerminatorMovies() - // Regular events for workflow progress + // Regular notifications for workflow progress go func() { - for block := range ev.BlockEvents() { + for block := range watcher.BlockNotifications() { fmt.Printf("Block: %s\n", block.BlockLabel) } }() go func() { - for movieText := range ev.MovieTextEvents() { + for movieText := range watcher.MovieTextNotifications() { fmt.Printf("Variable movie text: %s\n", movieText.Value) } }() - // Streaming events for real-time content + // Streaming notifications 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) + for stream := range watcher.MovieTextStreams() { + go func(inner <-chan watchers.VarNotification[string]) { + for notif := range inner { + fmt.Printf("Streaming content: %s\n", notif.Value) } }(stream) } }() - // Use single Events parameter + // Use single Watchers parameter results, err := b.DescribeTerminatorMovies(ctx, &b.DescribeTerminatorMoviesOptions{ - Events: ev, + Watchers: watcher, }) if err != nil { log.Fatal(err) @@ -911,9 +911,9 @@ func comprehensiveExample() { # Usage Scenarios -## Track events from subfunctions +## Track notifications 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`. +When your main workflow calls other BAML functions, you can track notifications from those subfunctions as well. If `MakePosts()` calls `Foo()`, and `Foo()` contains variables tagged with `@watch` or markdown blocks, the client invoking `MakePosts()` can subscribe to those subfunction notifications through dedicated records in the watcher. Consider this example where `MakePosts()` calls a helper function: @@ -927,7 +927,7 @@ function GeneratePostsWithProgress(url: string) -> Post[] { # Analyzing content let content = LLMAnalyzeContent(url); - let progress_status = "Starting generation" @emit; + let progress_status = "Starting generation" @watch; # Generate posts let posts = []; @@ -944,46 +944,46 @@ function GeneratePostsWithProgress(url: string) -> Post[] { ```python -# baml_client/events.py +# baml_client/watchers.py -class GeneratePostsWithProgressEventCollector: - """Event collector for GeneratePostsWithProgress function""" +class GeneratePostsWithProgress: + """Watcher for GeneratePostsWithProgress function""" - def on_block(self, handler: Callable[[BlockEvent], None]) -> None: - """Register a handler for block events from this function""" + def on_block(self, handler: Callable[[BlockNotification], None]) -> None: + """Register a handler for block notifications from this function""" pass - def on_var_progress_status(self, handler: Callable[[VarEvent[str]], None]) -> None: + def on_var_progress_status(self, handler: Callable[[VarNotification[str]], None]) -> None: """Register a handler for progress_status variable updates""" pass -class MakePostsEventCollector: - """Event collector for MakePosts function""" +class MakePosts: + """Watcher for MakePosts function""" def __init__(self): - self.function_GeneratePostsWithProgress = GeneratePostsWithProgressEventCollector() + self.function_GeneratePostsWithProgress = GeneratePostsWithProgress() ``` ```python # app.py from baml_client.sync_client import b -import baml_client.events as events +import baml_client.watchers as watchers def example(): - # Create the main event collector - ev = events.MakePostsEventCollector() + # Create the main watcher + watcher = watchers.MakePosts() - # Subscribe to subfunction events - ev.function_GeneratePostsWithProgress.on_var_progress_status( + # Subscribe to subfunction notifications + watcher.function_GeneratePostsWithProgress.vars.progress_status( lambda e: print(f"Subfunction progress: {e.value}") ) - ev.function_GeneratePostsWithProgress.on_block( + watcher.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}) + posts = b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher}) print(posts) ``` @@ -991,24 +991,24 @@ def example(): ```typescript -// baml_client/events.ts +// baml_client/watchers.ts -export interface GeneratePostsWithProgressEventCollector { - on_block(handler: (ev: BlockEvent) => void): void; - on_var_progress_status(handler: (ev: VarEvent) => void): void; +export interface GeneratePostsWithProgress { + on_block(handler: (notif: BlockNotification) => void): void; + on_var_progress_status(handler: (notif: VarNotification) => void): void; } -export interface MakePostsEventCollector { - function_GeneratePostsWithProgress: GeneratePostsWithProgressEventCollector; +export interface MakePosts { + function_GeneratePostsWithProgress: GeneratePostsWithProgress; } -export function MakePosts(): MakePostsEventCollector { +export function MakePosts(): MakePosts { return { function_GeneratePostsWithProgress: { - on_block(handler: (ev: BlockEvent) => void): void { + on_block(handler: (notif: BlockNotification) => void): void { // Implementation details }, - on_var_progress_status(handler: (ev: VarEvent) => void): void { + on_var_progress_status(handler: (notif: VarNotification) => void): void { // Implementation details } } @@ -1018,23 +1018,23 @@ export function MakePosts(): MakePostsEventCollector { ```typescript // index.ts -import { b, events } from "./baml-client" +import { b, watchers } from "./baml-client" async function example() { - // Create the main event collector - let ev = events.MakePosts() + // Create the main watcher + let watcher = watchers.MakePosts() - // Subscribe to subfunction events - ev.function_GeneratePostsWithProgress.on_var_progress_status((e) => { + // Subscribe to subfunction notifications + watcher.function_GeneratePostsWithProgress.on_var_progress_status((e) => { console.log(`Subfunction progress: ${e.value}`) }) - ev.function_GeneratePostsWithProgress.on_block((e) => { + watcher.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}) + const posts = await b.MakePosts("https://wikipedia.org/wiki/DNA", {"watchers": watcher}) console.log(posts) } ``` @@ -1043,64 +1043,64 @@ async function example() { ```go -// baml_client/events.go -package events +// baml_client/watchers.go +package watchers import "time" -type BlockEvent struct { +type BlockNotification struct { BlockLabel string `json:"block_label"` EventType string `json:"event_type"` Timestamp time.Time `json:"timestamp"` } -type VarEvent[T any] struct { +type VarNotification[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] +type GeneratePostsWithProgress struct { + blockNotifications chan BlockNotification + progressStatusNotifications chan VarNotification[string] } -func newGeneratePostsWithProgressEventCollector() *GeneratePostsWithProgressEventCollector { - return &GeneratePostsWithProgressEventCollector{ - blockEvents: make(chan BlockEvent, 100), - progressStatusEvents: make(chan VarEvent[string], 100), +func newGeneratePostsWithProgress() *GeneratePostsWithProgress { + return &GeneratePostsWithProgress{ + blockNotifications: make(chan BlockNotification, 100), + progressStatusNotifications: make(chan VarNotification[string], 100), } } -func (c *GeneratePostsWithProgressEventCollector) BlockEvents() <-chan BlockEvent { - return c.blockEvents +func (c *GeneratePostsWithProgress) BlockNotifications() <-chan BlockNotification { + return c.blockNotifications } -func (c *GeneratePostsWithProgressEventCollector) ProgressStatusEvents() <-chan VarEvent[string] { - return c.progressStatusEvents +func (c *GeneratePostsWithProgress) ProgressStatusNotifications() <-chan VarNotification[string] { + return c.progressStatusNotifications } -type MakePostsEventCollector struct { - blockEvents chan BlockEvent - progressPercentEvents chan VarEvent[int] - FunctionGeneratePostsWithProgress *GeneratePostsWithProgressEventCollector +type MakePosts struct { + blockNotifications chan BlockNotification + progressPercentNotifications chan VarNotification[int] + FunctionGeneratePostsWithProgress *GeneratePostsWithProgress } -func NewMakePostsEventCollector() *MakePostsEventCollector { - return &MakePostsEventCollector{ - blockEvents: make(chan BlockEvent, 100), - progressPercentEvents: make(chan VarEvent[int], 100), - FunctionGeneratePostsWithProgress: newGeneratePostsWithProgressEventCollector(), +func NewMakePosts() *MakePosts { + return &MakePosts{ + blockNotifications: make(chan BlockNotification, 100), + progressPercentNotifications: make(chan VarNotification[int], 100), + FunctionGeneratePostsWithProgress: newGeneratePostsWithProgress(), } } -func (c *MakePostsEventCollector) BlockEvents() <-chan BlockEvent { - return c.blockEvents +func (c *MakePosts) BlockNotifications() <-chan BlockNotification { + return c.blockNotifications } -func (c *MakePostsEventCollector) ProgressPercentEvents() <-chan VarEvent[int] { - return c.progressPercentEvents +func (c *MakePosts) ProgressPercentNotifications() <-chan VarNotification[int] { + return c.progressPercentNotifications } ``` @@ -1114,31 +1114,31 @@ import ( "log" b "example.com/myproject/baml_client" - "example.com/myproject/baml_client/events" + "example.com/myproject/baml_client/watchers" ) func main() { ctx := context.Background() - // Create the main event collector - ev := events.NewMakePostsEventCollector() + // Create the main watcher + watcher := watchers.NewMakePosts() // Consume subfunction streams as well as top-level updates. go func() { - for block := range ev.FunctionGeneratePostsWithProgress.BlockEvents() { + for block := range watcher.FunctionGeneratePostsWithProgress.BlockNotifications() { fmt.Printf("Subfunction block: %s\n", block.BlockLabel) } }() go func() { - for status := range ev.FunctionGeneratePostsWithProgress.ProgressStatusEvents() { + for status := range watcher.FunctionGeneratePostsWithProgress.ProgressStatusNotifications() { 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, + Watchers: watcher, }) if err != nil { log.Fatal(err) @@ -1162,105 +1162,105 @@ 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 +The same rule applies to variables tagged with `@watch`. Anything causing a change to a value will +cause the value to emit a notification to listeners that subscribe to the original variable. ```baml BAML -let x = Foo { name: "BAML" } @emit; // Make a BAML value that auto-emits +let x = Foo { name: "BAML" } @watch; // 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 +y.name = "Baml"; // Modify the new name => triggers notification -let a: int = 1 @emit; // Make a tracked BAML value +let a: int = 1 @watch; // Make a tracked BAML value let b = a; // Alias a to a new name -b++; // Modify the new name => No new event +b++; // Modify the new name => No new notification // (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 + on the other hand, wil not result in notifications 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. + old value, then the original variable will emit a notification. ## 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. +the value will continue to emit a notification. ```baml BAML -let x = Foo { name: "BAML" } @emit; // Make a tracked BAML value +let x = Foo { name: "BAML" } @watch; // Make a tracked BAML value let y = [x]; // Pack x into a list -y[0].name = "Baml"; // Modify the list item => triggers event +y[0].name = "Baml"; // Modify the list item => triggers notification ``` 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. +modifying `x` through `y[0]` above will also result in a notification being emitted. ## Track variables across function calls -When you pass an `@emit` variable to a function, there are two possible outcomes +When you pass a `@watch` 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. +1. The modification will immediately result in the firing of a notification. 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 +is doing some setup work that makes multiple changes to the watched value to build +it up to a valid result before the function returns, use Option 1 to hide the notifications 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 +and you are using notifications 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 + The notification-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. +To choose between modes, annotate the parameter with `@watch` in the function signature. ```baml BAML function Main() -> int { let state = Foo { name: "BAML", counter: 0, - } @emit; // Track state updates automatically + } @watch; // 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. +// notification, 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, +// marked with `@watch`, so `state` in `Main()` will fire two notifications, // one for each update of `s`. -function ChangeState(s: Foo @emit) -> null { +function ChangeState(s: Foo @watch) -> null { s.counter++; s.name = "Baml"; } ``` -# Comparison with other event systems +# Comparison with other observability 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 +The `@watch` system differs from many observability systems by focusing on automatic updates +and typesafe notification 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 @@ -1302,9 +1302,9 @@ def analyze_sentiments(phrases: List[str]) -> Response: 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. +With BAML's block notifications, we don't need to mix explicit logging with the workflow +logic. When a logged notification needs extra context (such as the index of an item being +processed from a list), we can use a `@watch` variable. ```baml BAML function LLMAnalyzeSentiment(message: string) -> string { ... } @@ -1316,7 +1316,7 @@ class Response { } function AnalyzeSentiments(messages: string[]) -> Response { - let status = "Starting analysis of " + messages.length().to_string() + " messages" @emit; + let status = "Starting analysis of " + messages.length().to_string() + " messages" @watch; sentiments = [] for i, message in enumerate(messages, 1): @@ -1392,9 +1392,9 @@ for await (const chunk of stream) { ``` 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. +from the pattern in BAML, where Workflow logic is separated from notification handling logic. -In BAML, functions and return values are meant for composing Workflow logic, while events +In BAML, functions and return values are meant for composing Workflow logic, while notifications are meant for communicating state back to your application. In the AI SDK, return values are used directly. @@ -1406,16 +1406,16 @@ are used directly. - 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 +**BAML's `@watch`:** +- Automatic notification generation from variable assignments +- Typesafe notification 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. +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 `@watch` system. ```typescript TypeScript (Mastra) // Mastra approach - watching workflow steps externally @@ -1430,7 +1430,7 @@ run.watch((event) => { 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. +With BAML's `@watch` system, you mark variables directly in your workflow logic and get typesafe notification 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 diff --git a/fern/03-reference/baml_client/runtime-events.mdx b/fern/03-reference/baml_client/runtime-events.mdx deleted file mode 100644 index 3072491523..0000000000 --- a/fern/03-reference/baml_client/runtime-events.mdx +++ /dev/null @@ -1,444 +0,0 @@ ---- -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/03-reference/baml_client/runtime-observability.mdx b/fern/03-reference/baml_client/runtime-observability.mdx new file mode 100644 index 0000000000..3043942dd7 --- /dev/null +++ b/fern/03-reference/baml_client/runtime-observability.mdx @@ -0,0 +1,459 @@ +--- +title: Runtime Observability +--- + + +This feature was added in 0.210.0 + + +The BAML runtime observability 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. + +## Notification Types + +### VarNotification + +Represents an update to a watched variable in your BAML workflow. + + + +```python +from typing import TypeVar, Generic +from baml_client.types import VarNotification + +T = TypeVar('T') + +class VarNotification(Generic[T]): + """ + Notification fired when a watched 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: +# VarNotification[int] for integer variables +# VarNotification[str] for string variables +# VarNotification[List[Post]] for complex types +``` + + + +```typescript +import type { VarNotification } from './baml-client/types' + +interface VarNotification { + /** + * Notification fired when a watched 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: +// VarNotification for integer variables +// VarNotification for string variables +// VarNotification for complex types +``` + + + +```go +package types + +import "time" + +// Since Go doesn't have user-defined generics, we generate specific types +// for each watched variable in your BAML functions + +// For a variable named "progress_percent" of type int +type ProgressPercentVarNotification 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 CurrentTaskVarNotification 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 CompletedPostsVarNotification struct { + VariableName string `json:"variable_name"` + Value []Post `json:"value"` + Timestamp time.Time `json:"timestamp"` + FunctionName string `json:"function_name"` +} +``` + + + +### BlockNotification + +Represents progress through a markdown block in your BAML workflow. + + + +```python +from baml_client.types import BlockNotification + +class BlockNotification: + """ + Notification 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 { BlockNotification } from './baml-client/types' + +interface BlockNotification { + /** + * Notification 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 BlockNotificationType string + +const ( + BlockNotificationEnter BlockNotificationType = "enter" + BlockNotificationExit BlockNotificationType = "exit" +) + +type BlockNotification 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 BlockNotificationType `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, watchers +from baml_client.types import VarNotification + +def track_progress(notif: VarNotification[int]): + print(f"Progress updated: {notif.value}% at {notif.timestamp}") + +def track_current_task(notif: VarNotification[str]): + print(f"Now working on: {notif.value}") + +# Set up variable tracking +watcher = watchers.MakePosts() +watcher.vars.progress_percent(track_progress) +watcher.vars.current_task(track_current_task) + +# Run the function +posts = await b.MakePosts("https://example.com", {"watchers": watcher}) +``` + + + +```typescript +import { b, watchers } from './baml-client' +import type { VarNotification } from './baml-client/types' + +const trackProgress = (notif: VarNotification) => { + console.log(`Progress updated: ${notif.value}% at ${notif.timestamp}`) +} + +const trackCurrentTask = (notif: VarNotification) => { + console.log(`Now working on: ${notif.value}`) +} + +// Set up variable tracking +const watcher = watchers.MakePosts() +watcher.on_var_progress_percent(trackProgress) +watcher.on_var_current_task(trackCurrentTask) + +// Run the function +const posts = await b.MakePosts("https://example.com", { watchers: watcher }) +``` + + + +```go +package main + +import ( + "fmt" + b "example.com/myproject/baml_client" + "example.com/myproject/baml_client/watchers" + "example.com/myproject/baml_client/types" +) + +func trackProgress(notif *types.ProgressPercentVarNotification) { + fmt.Printf("Progress updated: %d%% at %s\n", + notif.Value, notif.Timestamp.Format("15:04:05")) +} + +func trackCurrentTask(notif *types.CurrentTaskVarNotification) { + fmt.Printf("Now working on: %s\n", notif.Value) +} + +func main() { + ctx := context.Background() + + // Set up variable tracking + watcher := watchers.NewMakePosts() + + go func() { + for notif := range watcher.ProgressPercentNotifications() { + trackProgress(notif) + } + }() + + go func() { + for notif := range watcher.CurrentTaskNotifications() { + trackCurrentTask(notif) + } + }() + + // Run the function + posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{ + Watchers: watcher, + }) + if err != nil { + log.Fatal(err) + } +} +``` + + + +### Tracking Block Progress + + + +```python +from baml_client import b, watchers +from baml_client.types import BlockNotification + +def track_blocks(notif: BlockNotification): + indent = " " * (notif.block_level - 1) + action = "Starting" if notif.event_type == "enter" else "Completed" + print(f"{indent}{action}: {notif.block_label}") + +# Set up block tracking +watcher = watchers.MakePosts() +watcher.on_block(track_blocks) + +# Run the function +posts = await b.MakePosts("https://example.com", {"watchers": watcher}) +``` + + + +```typescript +import { b, watchers } from './baml-client' +import type { BlockNotification } from './baml-client/types' + +const trackBlocks = (notif: BlockNotification) => { + const indent = " ".repeat(notif.blockLevel - 1) + const action = notif.eventType === "enter" ? "Starting" : "Completed" + console.log(`${indent}${action}: ${notif.blockLabel}`) +} + +// Set up block tracking +const watcher = watchers.MakePosts() +watcher.on_block(trackBlocks) + +// Run the function +const posts = await b.MakePosts("https://example.com", { watchers: watcher }) +``` + + + +```go +func trackBlocks(notif *types.BlockNotification) { + indent := strings.Repeat(" ", notif.BlockLevel - 1) + action := "Starting" + if notif.EventType == types.BlockNotificationExit { + action = "Completed" + } + fmt.Printf("%s%s: %s\n", indent, action, notif.BlockLabel) +} + +func main() { + ctx := context.Background() + + // Set up block tracking + watcher := watchers.NewMakePosts() + + go func() { + for notif := range watcher.BlockNotifications() { + trackBlocks(notif) + } + }() + + // Run the function + posts, err := b.MakePosts(ctx, "https://example.com", &b.MakePostsOptions{ + Watchers: watcher, + }) + if err != nil { + log.Fatal(err) + } +} +``` + + + +## Generated Watcher API + + + +When you run `baml generate`, BAML analyzes your functions and creates type-safe notification handlers with generic types: + +```python +# For a function with `@watch let progress: int = 0` +watcher.vars.progress(callback: (notif: VarNotification[int]) -> None) + +# For a function with `@watch let status: string = "starting"` +watcher.vars.status(callback: (notif: VarNotification[str]) -> None) + +# For all markdown blocks +watcher.on_block(callback: (notif: BlockNotification) -> None) +``` + +The generic `VarNotification[T]` type provides compile-time type safety, ensuring your notification handlers receive the correct data types. + + + +When you run `baml generate`, BAML analyzes your functions and creates type-safe notification handlers with generic types: + +```typescript +// For a function with `@watch let progress: int = 0` +watcher.on_var_progress(callback: (notif: VarNotification) => void) + +// For a function with `@watch let status: string = "starting"` +watcher.on_var_status(callback: (notif: VarNotification) => void) + +// For all markdown blocks +watcher.on_block(callback: (notif: BlockNotification) => void) +``` + +The generic `VarNotification` interface provides compile-time type safety, ensuring your notification handlers receive the correct data types. + + + +When you run `baml generate`, BAML analyzes your functions and creates specific types for each watched variable (since Go doesn't have user-defined generics): + +```go +// Separate types generated for each watched variable +type ProgressVarNotification struct { + VariableName string + Value int + Timestamp time.Time + FunctionName string +} + +type StatusVarNotification struct { + VariableName string + Value string + Timestamp time.Time + FunctionName string +} + +// Corresponding notification channels +watcher.ProgressNotifications() <-chan *types.ProgressVarNotification +watcher.StatusNotifications() <-chan *types.StatusVarNotification +watcher.BlockNotifications() <-chan *types.BlockNotification +``` + +Each watched variable gets its own dedicated notification type, providing the same type safety as generics while working within Go's constraints. + + + +## Best Practices + +1. **Performance**: Keep notification handlers lightweight. They run sequentially in + a separate thread from the rest of the BAML runtime +1. **Error Handling**: Always include error handling in notification callbacks +1. **Naming**: Use descriptive names for watched variables to generate clear notification handler names + +## Related Topics + +- [Runtime Observability Guide](/guide/baml-advanced/runtime-observability) - Learn how to use notifications 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 15d98f65b7..e74c33024c 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -412,9 +412,9 @@ navigation: - page: Modular API icon: fa-regular fa-cubes path: 01-guide/05-baml-advanced/modular-api.mdx - - page: Runtime Events + - page: Runtime Observability icon: fa-regular fa-headset - path: 01-guide/05-baml-advanced/runtime-events.mdx + path: 01-guide/05-baml-advanced/runtime-observability.mdx - section: Boundary Cloud contents: # - section: Functions @@ -691,9 +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: Runtime Observability + slug: watchers + path: 03-reference/baml_client/runtime-observability.mdx - page: Multimodal slug: media path: 03-reference/baml_client/media.mdx diff --git a/result b/result deleted file mode 120000 index 8693364833..0000000000 --- a/result +++ /dev/null @@ -1 +0,0 @@ -/nix/store/l1342ji44qwx7xx1lsdk7z8qrh9yz1ax-baml-cli-0.211.0 \ No newline at end of file