Skip to content

feat: add per-turn state foundation (state scopes, container, loader) - #553

Merged
Lily Du (lilyydu) merged 8 commits into
mainfrom
lilyydu/01-state-foundation
Aug 24, 2026
Merged

feat: add per-turn state foundation (state scopes, container, loader)#553
Lily Du (lilyydu) merged 8 commits into
mainfrom
lilyydu/01-state-foundation

Conversation

@lilyydu

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

Copy link
Copy Markdown
Collaborator

This is PR 1 of 5 in the multi-connection OAuth stack. It introduces the per-turn state foundation independently so later PRs can add ctx.state, OAuth routing, and token-exchange deduplication.

This PR does not wire state into activity dispatch. That follows in PR 2.

What's included

TurnState

A Pythonic MutableMapping[str, Any] representing one conversation or user state scope.

  • Supports normal mapping operations such as state["key"], assignment, and deletion.
  • Detects nested dict and list mutations using content snapshots.
  • Avoids writes when content is unchanged or a mutation is reverted.
  • Seals after the turn so retained state cannot be accessed later.
  • Requires values to be JSON-serializable.

TurnStateContainer

Bundles the conversation scope and optional user scope with the identity they were loaded for.

  • Uses keyword-only fields.
  • Saves using the original conversation and user IDs.
  • Deletes backing storage before clearing in-memory state.
  • Marks cleared scopes clean so end-of-turn persistence does not repeat the deletion.

TurnStateLoader

Loads and persists scopes through the existing Storage abstraction.

  • Uses {prefix}:conv:{conversationId} and {prefix}:user:{conversationId}:{userId} keys.
  • Percent-encodes dynamic key segments to prevent ambiguous or colliding keys.
  • Persists each scope as a raw JSON object string.
  • Skips clean scopes and deletes dirty scopes that become empty.
  • Serializes all dirty scopes before performing storage writes, preventing serialization failures from partially updating state.
  • Marks scopes clean only after all requested storage operations succeed.
  • Treats unreadable or malformed values as missing and removes them from storage.
  • Accepts already-deserialized dictionaries for compatibility with storage providers.

Provider-owned storage behavior

The state layer keeps the existing Storage.get() / set() / delete() contract unchanged.

  • Expiration and other storage-specific behavior are configured directly on the provider supplied through StateOptions.storage.
  • State has no expiration by default.
  • Applications can provide a dedicated, preconfigured state storage instance when they need provider-specific policies.

Cross-SDK alignment

The implementation follows the same behavioral model as Teams SDK state in C# and TypeScript:

  • Conversation and user scopes.
  • Load once and save dirty scopes at the end of a turn.
  • Empty dirty scopes delete their backing keys.
  • State becomes unavailable after the turn.
  • Raw JSON scope persistence.
  • No expiration unless the configured provider applies it.

The public API remains Pythonic: TurnState behaves like a mapping rather than copying TypeScript's explicit get()/set() API. Python also automatically detects nested mutations so callers do not need to remember to reassign mutable values.

Validation

  • 37 focused state tests.
  • Coverage includes nested mutations, reverted changes, sealing, deletion, malformed data, escaped keys, save ordering, and successful-save re-baselining.
  • Build, lint, type-check, and supported Python-version checks pass.

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

Adds a new, standalone per-turn state subpackage (microsoft_teams.apps.state) that provides TurnState scopes (dirty tracking + sealing), a container for conversation/user scopes, and a loader that persists scopes through the shared Storage abstraction. This is positioned as the foundational state layer for the upcoming multi-connection OAuth stack, without wiring into dispatch yet.

Changes:

  • Introduces TurnState (MutableMapping) with sealing and dirty tracking.
  • Adds TurnStateContainer and TurnStateLoader (+ StateOptions) for load/save/delete over Storage.
  • Adds initial pytest coverage for state behavior and storage persistence semantics.

Reviewed changes

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

Show a summary per file
File Description
packages/apps/src/microsoft_teams/apps/state/turn_state.py Implements TurnState mapping with sealing/dirty tracking.
packages/apps/src/microsoft_teams/apps/state/container.py Adds TurnStateContainer for bundling scopes + identity and providing seal()/delete().
packages/apps/src/microsoft_teams/apps/state/loader.py Adds TurnStateLoader with key layout, JSON blob persistence, and TTL-on-load behavior.
packages/apps/src/microsoft_teams/apps/state/options.py Introduces StateOptions configuration (storage, key prefix, ttl).
packages/apps/src/microsoft_teams/apps/state/init.py Exposes the new state-layer public surface for microsoft_teams.apps.state.
packages/apps/tests/test_state.py Adds unit tests covering TurnState semantics and loader round-trips/TTL handling.

💡 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/state/loader.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/loader.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/container.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/turn_state.py
Comment thread packages/apps/src/microsoft_teams/apps/state/container.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/turn_state.py
Comment thread packages/apps/src/microsoft_teams/apps/state/container.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/loader.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/loader.py
Comment thread packages/apps/src/microsoft_teams/apps/state/loader.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/options.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/state/turn_state.py
Comment thread packages/apps/src/microsoft_teams/apps/state/options.py Outdated
@lilyydu
Lily Du (lilyydu) force-pushed the lilyydu/01-state-foundation branch from bc16aa4 to 85ec356 Compare August 13, 2026 21:23
Comment thread packages/apps/src/microsoft_teams/apps/state/loader.py
Comment thread packages/apps/src/microsoft_teams/apps/state/turn_state.py
Comment thread packages/apps/src/microsoft_teams/apps/state/loader.py Outdated
Lily Du (lilyydu) and others added 6 commits August 24, 2026 10:40
…sation_id/user_id

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…sation_id

TurnStateContainer is on the public API surface, so its constructor
signature is a compatibility contract. Make all fields keyword-only via
@DataClass(kw_only=True) so fields can be reordered or added later
without breaking callers, and drop the empty-string default on
conversation_id (it is the storage key identity — a silent "" default
would persist under a garbage key instead of failing loudly).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lilyydu
Lily Du (lilyydu) force-pushed the lilyydu/01-state-foundation branch from 84fefdb to d4f264d Compare August 24, 2026 17:40
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lilyydu
Lily Du (lilyydu) merged commit d3fb4ab into main Aug 24, 2026
8 checks passed
@lilyydu
Lily Du (lilyydu) deleted the lilyydu/01-state-foundation branch August 24, 2026 18:10
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.

4 participants