Not sure I agree with claude's points here will think it over:
   1. *State blob growth.* -- This depends on assumptions about modeling it
that claude made -- I think that if we model it closer to state it'll be
the same.
   2. *Resume-once under concurrency.* This is a whole can of worms we
don't really support a resume concurrency model anywhere? Why would this be
special? We'd need to get a lock etc...
   3. *Queryability.* Sure could be a boolean flag that is by default false
   4. *Schema coupling.* The state could be "waiting" effectively -- it
*is* state...

not saying I disagree with the approach necessarily but worth thinking
through

On Sun, Jun 7, 2026 at 3:10 PM Stefan Krawczyk <[email protected]>
wrote:

> Can we start some gists? Or draft PRs? That would help me if we talked UX
> and what it would mean. E.g. let's take that loyalty CRM case and look at
> the API one would use to implement it.
>
> On Mon, Jun 1, 2026, 12:41 PM André Ahlert <[email protected]> wrote:
>
> > Thanks Elijah, this is the right question to push on. Let me answer it
> head
> > on, because the answer is "yes there is an easier way, and I think we
> > should ship it as the default."
> >
> > You are right that a durable state can ride inside the existing state
> model
> > rather than a parallel structure. PR #786 already proves this: custom
> > persisters fall back to an in-state journal, and that path is correct. No
> > new persister methods, no dedicated tables, onboarding is zero. For a
> > single-worker workflow it is the simplest thing that works, and I do not
> > want to force anyone onto a heavier model to get suspend/resume.
> >
> > So I want to reframe the design as two tiers rather than one monolith:
> >
> >    - *Default (light):* journal and suspension records live in the
> existing
> >    state keyspace, exactly as you suggested. Any persister gets durable
> >    execution for free. This is the onboarding path.
> >    - *Opt-in (heavy):* a persister that implements the dedicated methods
> (
> >    save_suspension, mark_suspension_resolved, etc.) for workloads that
> >    outgrow the light path.
> >
> > The reason the opt-in tier earns its keep is not aesthetics, it is four
> > concrete costs the in-state path hits at scale:
> >
> >    1. *State blob growth.* Journal entries and suspension payloads inside
> >    state mean every save reserializes the whole blob. A workflow with N
> >    memoized sub-steps rewrites O(N) state on every step. Fine for small
> > flows,
> >    painful for long ones.
> >    2. *Resume-once under concurrency.* Two workers resuming the same
> >    suspension need an atomic claim. The SQL persisters expose exactly
> that
> >    primitive: mark_suspension_resolved is a conditional UPDATE ... WHERE
> >    resolved = 0 returning rowcount > 0, so under concurrency exactly one
> >    caller wins. In-state cannot express that primitive at all. To be
> > precise
> >    about the current state of the branch: resume() /  aresume() still
> gate
> >    on a non-atomic record.resolved read taken before the run and discard
> >    the bool, so the race is not yet closed end to end. Wiring resumption
> to
> >    skip the run unless mark_suspension_resolved returns True is part of
> >    this work, and it is only expressible on the dedicated tier (the
> > in-memory
> >    persister is a plain loop, not atomic).
> >    3. *Queryability.* "List all pending suspensions awaiting approval" is
> >    one SQL query against a table. In-state it is a full scan of every
> app's
> >    serialized state. Not viable for an ops dashboard or the Burr UI.
> >    4. *Schema coupling.* Mixing runtime metadata into the user state
> >    keyspace invites key collisions and muddies "what is my state" versus
> > "what
> >    is runtime bookkeeping."
> >
> > *The real case driving this.* A loyalty CRM I am running in production. A
> > campaign action generates a personalized offer per customer via an LLM,
> > processed as a sequential batch of ~50. Two things break today:
> >
> >    - Item 23 needs human approval before the offer goes out. Items 1
> >    through 22 have already paid for their LLM calls. With halt_before/
> >    halt_after I cannot pause mid batch, so I either fork state manually
> or
> >    re-run the batch and pay for 1 through 22 again.
> >    - On any crash or retry mid batch, every offer regenerates. The
> >    offer-generation LLM is the dominant cost in the workflow, so a single
> >    retry doubles the spend for that run.
> >
> > With __context.durable("offer", generate_offer, customer) the replay
> reads
> > the recorded offers and re-fires nothing. This is test-backed in the PR:
> > the integration suite asserts a durable side effect runs exactly once
> > across a suspend/resume cycle (the recorded value is replayed, the
> function
> > is not re-invoked). I also ran the HITL example end to end against a
> local
> > model to confirm the same behavior outside the test harness.
> >
> > Now, here is where the in-state default would actually hurt this case,
> > which is why I want the opt-in tier available: the recorded offer
> payloads
> > sit in the state blob and get reserialized on every subsequent step, and
> > the approval step is exactly where a second worker could pick up the same
> > suspension. Costs 1 and 2 are not hypothetical for this workload.
> >
> > *On the API surface.* I like your Azure-style yield ctx.durable(...) with
> > match. It reads cleanly. The current PR uses the callable form
> > ctx.durable(key,
> > fn, ...); I am open to the generator form if folks prefer it, the journal
> > semantics are the same underneath. And halt_when(durable=[...]) as thin
> > sugar over conditional edges is a nice touch, that also answers Stefan's
> > preference to keep halting expressed as state plus edges rather than a
> new
> > transition primitive.
> >
> > *On Stefan's hierarchy point:* the journal key is (partition_key, app_id,
> > sequence_id, step_key) today, with entries ordered by call_index. It does
> > not yet fold in a parent app id, so nested apps do not inherit a parent
> > journal. I will make the key compose with the parent app id so nested
> apps
> > inherit the journal correctly, which is the hierarchical reuse you
> flagged.
> >
> > Proposed path: keep suspend/resume and the journal in #786, make the
> > in-state path the documented default, mark the dedicated persister
> methods
> > as the opt-in scale tier, and address hierarchy in the same PR. If that
> > shape works for both of you I will split out anything that does not need
> to
> > land together.
> >
> > No deadline. Will send a [PROPOSAL] once we converge.
> >
> > PS: sorry for the blogpost in dev list, but is a big theme. :)
> >
> > André
> >
> > Em dom., 31 de mai. de 2026 às 22:58, Elijah ben Izzy <
> > [email protected]> escreveu:
> >
> > > This is very cool. So I thought through there's definitely a need here
> to
> > > pause mid-action.
> > >
> > > Some API ideas:
> > > 1. Put durable_keys as part of the declaration to match the type-safe,
> > > declarative nature of the rest of it
> > > 2. Use something clever (i.e. match statements) to make it easier to
> > match
> > > the mental model
> > >
> > > Azure durable functions has an interesting approach:
> > >
> > >   def my_action(state, ctx):
> > >       user = yield ctx.durable("fetch", db.get_user, state["uid"])
> > >       match user.tier:
> > >           case "gold":
> > >               result = yield ctx.durable("gold_flow", gold_flow, user)
> > >           case _:
> > >               result = yield ctx.durable("std_flow", std_flow, user)
> > >       return state.update(result=result)
> > >
> > > Worth exploring. I'm also wondering if there's a simple way to add an
> > > optional field to the data model rather than a whole new "supports
> > > durable"? I.E. would there be an easier way to onboard? Could we model
> it
> > > as some sort of sub-state that we could store and pass in? If we had
> > > durable things as a special key-space in the state could we just use
> the
> > > same ones? Trade-offs not sure the right one.
> > >
> > > The problem is that if we don't have suspend() we decouple it but need
> a
> > > mechanism to halt execution. Might be a good place for syntactic sugar
> --
> > > halt_during? halt_when(durable=["key"])?
> > >
> > > With iterator-based APIs it's actually up to the user to handle the
> > halting
> > > -- this completely bypasses this issue (we just send out a durable-set
> > > event mid-stride as we would an iterator or an iterator of streams).
> > >
> > >
> > >
> > > On Sun, May 31, 2026 at 11:45 AM Stefan Krawczyk <
> > > [email protected]>
> > > wrote:
> > >
> > > > If halt_when has clear semantics if what happens next then that
> sounds
> > > > good. But I haven't found a clear way to make that obvious. So I
> think
> > > > making users set up state and conditional edges should be the cleaner
> > > model
> > > > (we should also show that running a graph with halt* is fine in
> > examples
> > > > and drop that warning).
> > > >
> > > > Durable key seems fine I think. I think the hierarchical nature
> should
> > be
> > > > utilized that we've set up, so more capabilities to enable that way
> of
> > > > pausing and resuming to be more useful makes sense to me -- IIUC.
> > > >
> > > > On Thu, May 28, 2026, 6:14 AM André Ahlert <[email protected]>
> wrote:
> > > >
> > > > > Hi all,
> > > > >
> > > > > PR #786 <https://github.com/apache/burr/pull/786> [1] proposes two
> > > > > additions to handle long-running agent workloads. Moving the design
> > > > > question from DM with Stefan onto the list before we iterate
> further.
> > > > >
> > > > > Gap today: halt_before / halt_after only stop between actions. Two
> > > cases
> > > > > keep showing up:
> > > > >
> > > > >    1. HITL inside one action (e.g. sequential batch where item 23
> of
> > 50
> > > > >    needs approval, items 1-22 already paid for LLM calls).
> > > > >    2. Crash recovery mid-action without re-firing paid sub-steps.
> > > > >
> > > > > Langgraph covers both with interrupt() + checkpointer. Concrete
> case
> > I
> > > am
> > > > > hitting: a loyalty CRM where every retry re-fires the
> > offer-generation
> > > > LLM.
> > > > >
> > > > > Proposal is to split #786 into two orthogonal pieces, ship
> > > independently:
> > > > >
> > > > > * A. *halt_when(predicate): state-based HITL primitive. Action
> sets a
> > > > state
> > > > > value, runtime halts when predicate matches, conditional edges
> route
> > on
> > > > > resume. No new resume semantics. (Stefan's suggested shape on the
> > PR.)
> > > > >
> > > > > * B. *__context.durable(key, fn): sub-step memoization journal.
> > Result
> > > > > keyed by (app_id, sequence, key). On replay, returns recorded value
> > > > without
> > > > > re-firing fn. Defensible on crash recovery alone.
> > > > >
> > > > > Worked example (before/after code, mid-action case discussion):
> > > > > https://gist.github.com/
> > > > >
> > > > > Open questions:
> > > > >
> > > > >    1. halt_when(predicate) as first-class transition primitive, or
> > keep
> > > > as
> > > > >    user-side conditional edge expression?
> > > > >    2. __context.durable(key, fn) the right surface, or decorator?
> > > > >    3. Anyone hitting the mid-action sequential case in production?
> > > > >
> > > > > Not proposing the suspend()-style mid-action pause in this thread.
> > Want
> > > > to
> > > > > ship A and B, collect signal, revisit on a separate [DISCUSS] if
> > demand
> > > > > shows up.
> > > > >
> > > > > No deadline. Will follow up with [PROPOSAL] per piece once we
> > converge.
> > > > >
> > > > > [1] https://github.com/apache/burr/pull/786
> > > > >
> > > > > Thanks, André
> > > > >
> > > > >
> > > >
> > >
> >
>

Reply via email to