workshops/2025-05/sections/01-cli-and-agent/README.md
Now let's add BAML and create our first agent with a CLI interface.
First, we'll need to install BAML which is a tool for prompting and structured outputs.
npm install @boundaryml/baml
Initialize BAML
npx baml-cli init
Remove default resume.baml
rm baml_src/resume.baml
Add our starter agent, a single baml prompt that we'll build on
cp ./walkthrough/01-agent.baml baml_src/agent.baml
// ./walkthrough/01-agent.baml
class DoneForNow {
intent "done_for_now"
message string
}
function DetermineNextStep(
thread: string
) -> DoneForNow {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a helpful assistant that can help with tasks.
{{ _.role("user") }}
You are working on the following thread:
{{ thread }}
What should the next step be?
{{ ctx.output_format }}
"#
}
test HelloWorld {
functions [DetermineNextStep]
args {
thread #"
{
"type": "user_input",
"data": "hello!"
}
"#
}
}
Generate BAML client code
npx baml-cli generate
Enable BAML logging for this section
export BAML_LOG=debug
Add the CLI interface
cp ./walkthrough/01-cli.ts src/cli.ts
// ./walkthrough/01-cli.ts
// cli.ts lets you invoke the agent loop from the command line
import { agentLoop, Thread, Event } from "./agent";
export async function cli() {
// Get command line arguments, skipping the first two (node and script name)
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Error: Please provide a message as a command line argument");
process.exit(1);
}
// Join all arguments into a single message
const message = args.join(" ");
// Create a new thread with the user's message as the initial event
const thread = new Thread([{ type: "user_input", data: message }]);
// Run the agent loop with the thread
const result = await agentLoop(thread);
console.log(result);
}
Update index.ts to use the CLI
src/index.ts
+import { cli } from "./cli"
+
async function hello(): Promise<void> {
console.log('hello, world!')
async function main() {
- await hello()
+ await cli()
}
cp ./walkthrough/01-index.ts src/index.ts
Add the agent implementation
cp ./walkthrough/01-agent.ts src/agent.ts
// ./walkthrough/01-agent.ts
import { b } from "../baml_client";
// tool call or a respond to human tool
type AgentResponse = Awaited<ReturnType<typeof b.DetermineNextStep>>;
export interface Event {
type: string
data: any;
}
export class Thread {
events: Event[] = [];
constructor(events: Event[]) {
this.events = events;
}
serializeForLLM() {
// can change this to whatever custom serialization you want to do, XML, etc
// e.g. https://github.com/got-agents/agents/blob/59ebbfa236fc376618f16ee08eb0f3bf7b698892/linear-assistant-ts/src/agent.ts#L66-L105
return JSON.stringify(this.events);
}
}
// right now this just runs one turn with the LLM, but
// we'll update this function to handle all the agent logic
export async function agentLoop(thread: Thread): Promise<AgentResponse> {
const nextStep = await b.DetermineNextStep(thread.serializeForLLM());
return nextStep;
}
The the BAML code is configured to use OPENAI_API_KEY by default
As you're testing, you can change the model / provider to something else as you please
client "openai/gpt-4o"
Docs on baml clients can be found here
For example, you can configure gemini or anthropic as your model provider.
If you want to run the example with no changes, you can set the OPENAI_API_KEY env var to any valid openai key.
export OPENAI_API_KEY=...
Try it out
npx tsx src/index.ts hello
you should see a familiar response from the model
{
intent: 'done_for_now', message: 'Hello! How can I assist you today?' }