Skip to content

feat: expose per-turn state (ctx.state, App(state=...)) and wire into dispatch - #554

Open
Lily Du (lilyydu) wants to merge 2 commits into
lilyydu/01-state-foundationfrom
lilyydu/02-state-api
Open

feat: expose per-turn state (ctx.state, App(state=...)) and wire into dispatch#554
Lily Du (lilyydu) wants to merge 2 commits into
lilyydu/01-state-foundationfrom
lilyydu/02-state-api

Conversation

@lilyydu

@lilyydu Lily Du (lilyydu) commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is PR 2 of 5 in the multi-connection OAuth stack. PR #553 lays out the state foundation, whereas this PR uses it - it exposes the public API, adds the App(state=...) opt-in, and wires load/save around every turn in the activity processor. Behavior mirrors the C# Microsoft.Teams.Apps.State integration so the SDKs stay aligned.

When state is disabled (the default), everything short-circuits: the loader is None, ctx.state is None, and there is no per-turn overhead.

What changed

Opt-in (options.py) — new state: Optional[Union[bool, StateOptions]] on AppOptions/InternalAppOptions. None/False → off; True → app's shared storage; StateOptions(...) → custom prefix/TTL/store.

Resolution (state/loader.py) — new create_state_loader(state, fallback_storage) factory: the whole bool | StateOptions | None → Optional[TurnStateLoader] decision in one place; warns when the resolved store is in-memory LocalStorage.

Wiring (app.py) — resolves self._state_loader at construction, passes it to ActivityProcessor.

Dispatch lifecycle (app_process.py)_load_turn_state before the try-block, _persist_turn_state in a finally (saves even on handler error, then seals).

Public surfaceActivityContext.state; re-exports StateOptions, TurnState, TurnStateContainer, TurnStateSealedError, create_state_loader.

Exampleexamples/state/ with a per-conversation counter + per-user name.

@lilyydu
Lily Du (lilyydu) marked this pull request as ready for review August 11, 2026 17:35
Copilot AI lite review requested due to automatic review settings August 11, 2026 17:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires the previously introduced per-turn state layer into the microsoft_teams.apps dispatch lifecycle and exposes the opt-in public API (App(state=...), ctx.state) so application handlers can read/write conversation/user state across turns (aligned with the C# SDK behavior).

Changes:

  • Add App(state=...) opt-in via new state option on AppOptions/InternalAppOptions and resolve it to a TurnStateLoader.
  • Load per-turn state onto ctx.state before handler dispatch and persist+seal it at the end of each turn; add tests covering disabled/enabled/sealed/error-save behavior.
  • Add a new examples/state sample app and register it in the workspace lockfile.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
uv.lock Registers the new examples/state workspace member.
packages/apps/tests/test_state.py Adds unit tests for create_state_loader resolution logic and warning behavior.
packages/apps/tests/test_app_process.py Adds integration tests for ctx.state lifecycle (disabled/enabled/persist/seal/error).
packages/apps/src/microsoft_teams/apps/state/loader.py Introduces create_state_loader(...) factory for App(state=...) resolution.
packages/apps/src/microsoft_teams/apps/state/init.py Re-exports create_state_loader on the apps.state package surface.
packages/apps/src/microsoft_teams/apps/routing/activity_context.py Adds ActivityContext.state field for per-turn state access.
packages/apps/src/microsoft_teams/apps/options.py Adds state option to app configuration with documentation.
packages/apps/src/microsoft_teams/apps/app.py Resolves and passes state_loader into ActivityProcessor.
packages/apps/src/microsoft_teams/apps/app_process.py Wires load/save/seal of per-turn state into the dispatch lifecycle.
packages/apps/src/microsoft_teams/apps/init.py Re-exports state types on the public microsoft_teams.apps surface (currently missing create_state_loader).
examples/state/src/main.py Adds a runnable example demonstrating per-conversation and per-user state usage.
examples/state/README.md Documents how to run the new state example and expected behavior.
examples/state/pyproject.toml Declares the example app project dependencies and workspace source.
Suppressed comments (2)

packages/apps/src/microsoft_teams/apps/app_process.py:322

  • Because _persist_turn_state() is awaited in a finally, any exception while saving state can override an exception raised by the handler/middleware, changing the error that callers observe. Consider catching persistence failures in the finally (and logging / emitting on_error) so they don’t mask the primary failure.
        finally:
            await self._persist_turn_state(activityCtx)

packages/apps/src/microsoft_teams/apps/init.py:53

  • create_state_loader is not included in __all__, so it won’t be exported from microsoft_teams.apps even if imported. Add it to the export list if it’s meant to be part of the public surface.
    "StateOptions",
    "TurnState",
    "TurnStateContainer",
    "TurnStateSealedError",
    "to_threaded_conversation_id",

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/apps/src/microsoft_teams/apps/app_process.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/app_process.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/app_process.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/__init__.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants