microsoft / microsoft/agent-framework-durable-extension

Workflow context_filter runs inside orchestrator replay

Open
#79 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement python workflows
Dominant language
Python
Stars
16
Forks
10
Avg merge
3d 9h
Merged PRs (30d)
9

Description

What happens

AgentExecutor.context_filter is invoked from _build_context_messages in
_workflows/orchestrator.py, which is reached from run_workflow_orchestrator. That function
yields on ctx.task_all and ctx.wait_for_external_event, so it is replayed orchestrator code.

A durable orchestrator does not resume, it re-executes from the top on every episode. The filter
therefore runs once per replay rather than once per handoff: roughly once per node in a sequential
workflow, and again each time a workflow parked on a human decision wakes.

Core types the filter as Callable[[list[Message]], list[Message]] and requires nothing further,
because an in-process executor runs it exactly once. Durable silently imposes a stricter contract.

It fails softly, which bounds the severity

Verified against durabletask/worker.py. Non-determinism is detected by checking that an action
exists at the expected id and is of the expected kind:

action = ctx._pending_actions.pop(entity_call_id, None)
if not action:
    raise _get_non_determinism_error(...)
elif not action.HasField("sendEntityMessage") or not ...

The action's input is never compared, and the projection is only ever an input with nothing
branching on it. So a divergent filter raises no NonDeterminismError and delivers no altered
context to an agent. The recomputed value is discarded and the recorded result stands.

What does bite, in order:

  1. Side effects repeat on every replay, so one handoff can write many audit entries.
  2. I/O can raise on a later replay, failing an orchestration whose original run succeeded and whose
    result is already recorded.
  3. Slow filters are paid for per episode rather than once.

Only custom is exposed. full and last_agent are list slicing.

The placement trade-off

Where the projection is applied On the wire User code in replay
Orchestrator (today) Projection only Yes
At the destination Whole conversation No
Inside an activity Projection only No, one extra round trip

The current design took the first deliberately, because keeping only the projection on the wire is
what makes context_mode an effective capacity lever. Measured, at 800 turns full is 675,560
bytes against 845 for last_agent.

Work this tracks

  • Upstream: public accessor for context_mode / context_filter on AgentExecutor.
    Durable reads the private attributes today with a full fallback. Guarded by projection tests
    that build a real AgentExecutor per mode, so a rename fails CI, but it should not need guarding.

  • Decide internally: whether the accessor is sufficient, or whether the projection should
    also be applied inside an activity for custom mode so it is recorded once.

    Applying it in an activity removes the purity requirement entirely, at the cost of a scheduling
    round trip per handoff. Not obviously worth it, since violating the contract fails softly today.
    The accessor alone may be enough, given the contract is now documented.

Not pursued

Asking core to require purity of context_filter for all users. The constraint comes from replay,
which only durable has, so durable owns the delta. Documented in ADR 0032.

Already done

The durable contract is documented in ADR 0032 under "context_filter must be pure under durable":
synchronous, deterministic, free of side effects, and independent of wall-clock time, randomness and
external state.

Raised by @larohra in review of #59.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with _workflows/orchestrator.py and ADR 0032, then inspect durabletask/worker.py and the existing projection tests that construct AgentExecutor instances. Decide whether a public context_mode/context_filter accessor is sufficient or whether custom projection belongs in an activity; done means the chosen design is documented and covered by tests without relying on private attributes.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, distributed-systems
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.