Skip to content

Long-Poll Flow Bridging, the Bank Support & Flight Booking Agents, JWKS Hardening

Latest

Choose a tag to compare

@bw19 bw19 released this 07 Jul 06:28

v1.45.0 changes how a caller bridges a durable flow to a bounded request. The foreman's flow-stop notificationFlowOptions.NotifyOnStop and the OnFlowStopped outbound event — is removed in favor of a new long-poll: Poll waits up to the request's time budget for a flow to stop, but a timeout is not an error, so a still-running flow comes back as a non-error running outcome and the caller can answer within its budget and re-poll immediately. Two new example microservices are built on it: banksupport.example, an authentication-and-structured-output agent whose LLM tools are scoped to the signed-in customer's own data, and flightbooking.example, a human-in-the-loop agent that parks on a real Interrupt/Resume decision and composes a nested seat-selection subgraph. The release also hardens token verification — a rotated signing key is now published in JWKS before it signs anything, and per-issuer JWKS fetches are debounced so tokens carrying garbage kids cannot amplify into a fetch storm — and fixes a data race in the connector's SetConfig.

Highlights

  • Poll and PollAndParse on the foreman. The long-poll sibling of Await: it waits up to the request's time budget for the flow to stop, but returns a still-running flow as a non-error outcome (outcome.Stopped() == false) so a caller bridging an open-ended flow to a bounded HTTP request can answer within its budget and re-poll with no client-side delay.
  • Flow-stop notification is removed. FlowOptions.NotifyOnStop and the foreman's OnFlowStopped outbound event are gone; the launch-then-poll pattern replaces them. (Breaking — see below.)
  • The banksupport.example agent. A single-microservice showcase of JWT authentication, actor-claims authorization, actor-scoped LLM tools (confused-deputy safe), a durable Support workflow, and typed structured LLM output.
  • The flightbooking.example agent. A durable BookFlight workflow that parks on a real human accept/keep-searching decision via flow.Interrupt/foreman.Resume, teaches graph-composition-vs-subgraph, and nests a seat-selection child workflow.
  • JWKS verification hardening. Rotated keys are advertised in JWKS for a short activation delay before they sign anything, and connector JWKS refreshes are debounced per issuer (at most one fetch per second) so unknown-kid floods cannot amplify into bus calls against the token services. The JWKS response is marked cacheable for one second.
  • SetConfig data race fixed. The connector now holds configLock across the whole read-modify-write of a config value instead of releasing it mid-update, closing a race with Config() and config refresh.
  • genservice literal guards. The generator now errors on a non-literal string-valued define.* field (a const or expression is silently dropped by the AST reader — e.g. leaving an endpoint ungated) and on a backtick inside a feature's godoc (which would break the generated raw string literal).

Bridging Open-Ended Flows with Poll

A flow can outlast a single request's time budget — an LLM tool-calling loop, a long human-in-the-loop pause, a slow downstream. Previously a caller could opt a flow into an asynchronous OnFlowStopped notification; that mechanism is removed. The replacement is Poll, launched after a plain Create:

flowKey, err := foremanapi.NewClient(svc).Create(ctx, myapi.Support.URL(), in, nil)
// ... store flowKey; return it to the browser ...

// From a status endpoint the caller polls:
outcome, err := foremanapi.NewClient(svc).Poll(ctx, flowKey)
if !outcome.Stopped() {
    // Still running — report "running" and let the caller re-poll immediately.
}

Poll waits up to the request's time budget for the flow to stop. Unlike Await, a timeout is not an error: a still-running flow returns a non-error outcome with outcome.Stopped() == false, so the connection is held efficiently for the duration of the budget and the caller re-fetches with no fixed-interval delay. A completed flow's outcome is delivered the instant it finishes. PollAndParse additionally unmarshals the terminal state into a typed struct, the way AwaitAndParse does for Await. Await and Run remain the synchronous, block-until-stopped forms for flows that finish within the request budget.

Two New Agent Examples

banksupport.example is the suite's authentication + structured-output showcase. A signed-in customer asks a natural-language banking question and an LLM agent answers by calling the customer's own Balance and Transactions endpoints as tools, returning a typed verdict (advice, a card-block flag, a 0–10 risk score). The tools take no customer-id argument — each reads the identity from the verified actor claim and is gated by requiredClaims: roles.customer, so the model can only ever read the signed-in customer's own data. The agent is a durable Support workflow rather than a synchronous Chat, because a multi-turn tool-calling loop outlasts one request's budget; the browser learns the result by long-polling the foreman's new Poll. Demo accounts (alice, bob) and their histories live in a deterministically-seeded, read-only in-memory store so every replica agrees.

flightbooking.example is the human-in-the-loop showcase. Its BookFlight workflow searches a route, proposes candidate flights one at a time, and parks on a real flow.Interrupt awaiting a human accept-or-keep-searching decision, resumed via foreman.Resume. It teaches the graph-composition-vs-subgraph distinction on purpose: the propose/decide/confirm nodes share one graph, while seat selection is delegated to an isolated ChooseSeatAgent child workflow that itself runs llm.core's ChatLoop as a further subgraph. The LLM only refines the seat, so the headline — Interrupt/Resume, a goto loop, nested subgraphs — is fully demonstrable with no provider key; without one, seat selection falls back to the first available seat.

Both examples are wired into the examples app by default. The standalone structured-output and bank-account CRUD examples are subsumed by banksupport.

Token Verification Hardening

The token-verification hot path is hardened against unknown-kid flood amplification and made more forgiving of key rotation:

  • Delayed key activation. When a signing key rotates, the new key is published in JWKS for a short activation delay before it signs any token — the access token service keeps signing with its previous key and the bearer token service with its alternate (outgoing) key during the window. Verifiers therefore have time to fetch the new key before the first token bearing its kid arrives, eliminating a class of transient verification failures right after a rotation.
  • Debounced JWKS fetches. A connector refreshes JWKS on an unknown kid, but that refresh is now debounced per issuer (at most once per second), so a flood of tokens carrying garbage kids cannot amplify into a storm of JWKS fetches against access.token.core / bearer.token.core. The JWKS response is marked cacheable for one second for the same reason.

Neither change adds a configuration knob; the activation delay and fetch cooldown are fixed constants.

Other Changes

  • Connector SetConfig data race. SetConfig looked up a config under configLock but then read the original value and wrote the new one after releasing it, racing Config() and the config-refresh path. The lock is now held across the whole read-modify-write and released before the onConfigChanged callback, using a pre-computed changed flag so the callback gate no longer reads shared state.
  • Transport teardown read. The NATS connection refcount is now read inside poolLock so the teardown decision is a single synchronized observation.
  • Dependencies. Dwarf is updated to v0.9.3 (which adds the foreman Poll primitive), alongside a general dependency refresh.

Breaking Changes

  • Flow-stop notification is removed. FlowOptions.NotifyOnStop and the foreman's OnFlowStopped outbound event no longer exist. A microservice that subscribed to OnFlowStopped to learn when a flow stopped should instead launch the flow with Create and long-poll it with Poll (or block with Await/Run when the flow finishes within the request budget). Remove the OnFlowStopped inbound event sink and drop NotifyOnStop from any FlowOptions.

Migration

From inside a Microbus project, ask Claude Code to upgrade Microbus:

Get the latest version of Microbus.

The upgrade system is now a single self-propagating skill. Each release ships one self-contained upgrade-microbus skill that hardcodes its source and destination versions: it bumps go.mod to the destination, applies that release's source edits, regenerates with genservice, and gates the increment with go vet. If a farther target is requested, the skill pulls the next published release's copy of itself and chains hop-by-hop until it lands, running go test once at the target as a best-effort behavior check. For v1.45.0 the migration removes the workflow flow-stop notification — the OnFlowStopped sink and FlowOptions.NotifyOnStop — and points the affected callsites at the launch-then-Poll pattern.

Documentation