microsoft / microsoft/agent-framework-durable-extension
Workflow context_filter runs inside orchestrator replay
Nobody has claimed this yet.
- 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:
- Side effects repeat on every replay, so one handoff can write many audit entries.
- I/O can raise on a later replay, failing an orchestration whose original run succeeded and whose
result is already recorded. - 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_filteronAgentExecutor.
Durable reads the private attributes today with afullfallback. Guarded by projection tests
that build a realAgentExecutorper 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 forcustommode 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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