Skip to content

Flows, agents & turns

Building an agent

go
agent := bb.NewAgent().
    WithModel(bb.NewModel("fast")).
    WithRole(bb.Role("You are Jarvis.")).
    WithSchema(bb.Schema[intent]()).      // optional: expect structured output
    Selects(idTalk, idHouse).             // optional: declare Select exits
    OnMessage(func(ctx context.Context, turn bb.Turn, chat bb.ModelChat) error {
        chat.Add(turn.Last())             // add the latest incoming message
        reply, err := chat.Ask()          // send role + added chat to the model
        if err != nil {
            return err                    // schema mismatch + transport surface here
        }
        turn.Reply(reply.ReadAll())       // add an assistant message to the flow
        return nil
    })
  • turn.Messages is the incoming conversation; turn.Last() is the latest.
  • chat.Add(msgs…) chooses what the next Ask sends; chat.AskWith(msgs…) is Add+Ask.
  • turn.Reply(text) appends output to the flow's chat (zero or many times); it does not go to the model.
  • reply.ReadAll() / Read() / Stream() read the answer; bb.Extract[T](reply) decodes it into a schema type.
  • ctx is this turn's context; it is done when the handler returns. Pass it to any I/O you do so cancellation is respected.
  • turn.Request() is the client's request as context: the sampling parameters it sent (model, temperature, top_p, max_tokens, stop, think, …). They are not applied to your agent automatically — they are an input for the handler to read and act on.

Request parameters are input, never an override

The engine never silently honors a client's sampling knobs; it hands them to the flow so you decide. A handler can honor, clamp, ignore, or branch on them:

go
OnMessage(func(ctx context.Context, turn bb.Turn, chat bb.ModelChat) error {
    req := turn.Request()
    if n := req.MaxTokens; n != nil && *n < 64 {
        chat.Add(bb.NewMessage("Answer in one short sentence.").As("system"))
    }
    // req.Model is the model id the client asked for (also what selects a
    // named flow at the serving layer); req.Temperature/TopP/Stop are theirs
    // to weigh in. req.TopK is Anthropic-only (nil on OpenAI requests).
    // req.MaxTokens already resolves OpenAI's deprecated max_tokens vs. the
    // current max_completion_tokens for you — one field either way.
    // req.Tools() / req.ToolChoice() are the tools the client declared.
    // req.Think (Anthropic "thinking", OpenAI "reasoning_effort") is nil when
    // the client sent no opinion; non-nil is a request, not a command — the
    // agent's own model decides whether WithThink means anything to it.
    chat.Add(turn.Last())
    reply, err := chat.Ask() // asks with the agent's own model config, not req's
    // ...
})

The agent's own WithModel config is what Ask uses; the request params are context, never an override — so a brain stays a brain, not a raw model whose behavior the caller dictates.

Structured output

WithSchema(bb.Schema[T]()) tells the agent to expect JSON matching T; Ask validates the reply against it (a mismatch is the error from Ask), and bb.Extract[T](reply) returns a typed T:

go
type intent struct {
    Intent string `json:"intent" enum:"talk,house,remember" doc:"the chosen capability"`
    Reason string `json:"reason"`
}
// ...
reply, err := chat.Ask()
if err != nil { return err }
it := bb.Extract[intent](reply)
turn.Select(it.Intent)

Struct tags shape the schema sent to the model: doc:"…" becomes a field description, and enum:"a,b,c" constrains a field to a fixed set — handy for a router that must pick one of a known list of ids.

bb.Extract is a free function (not reply.Extract[T]()) because Go forbids type parameters on methods — the same shape as bb.Schema[T](). It reads a tool call's arguments too: bb.Extract[sensorArgs](call).

Talking to a model with no flow at all

The same ModelChat works on its own, which is useful in tests, scripts, or inside a Go tool a flow calls:

go
reply, err := bb.Chat(ctx, bb.NewModel("smart")).AskWith(bb.NewMessage("hi"))

Chaining and continuing past the reply

a.Next(b).Next(c) runs a→b→c, threading the chat. bb.Respond is the prebuilt flow that delivers everything produced since the previous Respond (or since the start) as one stage of the answer; you can chain flows after it to keep acting:

go
brain := router.Next(bb.Select(caps...)).Next(bb.Respond).Next(notify)

bb.Respond is repeatable — a chain may contain several, and each is a stage boundary the client sees as soon as it's ready:

go
// A -> B -> respond -> C -> D -> respond -> E
brain := flowA.Next(flowB).Next(bb.Respond).           // stage 1: B's reply
    Next(flowC).Next(flowD).Next(bb.Respond).           // stage 2: C's + D's replies
    Next(flowE)                                         // initiative; not part of the answer

Only flows after the last Respond are pure initiative. A streaming client sees each stage's tokens as they're produced; a non-streaming client gets every stage's text, joined with a blank line between stages. A stage with nothing new since the previous boundary answers with nothing for that stage — it never falls back to echoing the client's own message.

bb.Notify(send) is a prebuilt outgoing flow — it sends the chat's last message to send and passes the chat through:

go
notify := bb.Notify(func(ctx context.Context, text string) error {
    return postToWebhook(ctx, text)
})

Naming and models on any flow

WithId and WithModel are methods of every flow, not just NewFlow() — a Select, an All/One/Group, or a Next chain too. Name a composite to make it one addressable unit (Selectable, triggerable, durable); set a model on a group to give the agents inside it a default:

go
capabilities := bb.Select(talk, remember, house).
    WithModel(bb.NewModel("cheap")). // default for member agents that set none
    WithId("capabilities")            // name the whole group

Model resolution is lexical scope over the tree: agent's own → its flow's → nearest enclosing group's → bb.WithDefaultModel → first registered. Because these return the Flow interface, call them after the Basic-only WithAgent (NewFlow().WithAgent(a).WithModel(m).WithId("x")).

Next: Models & roles for exactly how that ladder resolves, or Routing with Select for multi-capability brains.

A tree of flows and agents, disguised as a model.