[agentserver-core] Add FoundryStateStore durable KV storage layer#47763
[agentserver-core] Add FoundryStateStore durable KV storage layer#47763shanmukha1200 wants to merge 1 commit into
Conversation
|
Thank you for your contribution @shanmukha1200! We will review the pull request and get back to you soon. |
There was a problem hiding this comment.
Pull request overview
This PR adds a new protocol-neutral storage layer (azure.ai.agentserver.core.storage) to azure-ai-agentserver-core. It introduces FoundryStorageClient (a base owning the AsyncPipelineClient, policy chain, and error handling) and FoundryStateStore, a generic durable key-value store over POST /storage/state:read|:write|:listKeys with namespace/key/value/tags, optional if_match optimistic concurrency, and ordered, paged list_keys. This generalizes the existing responses-package FoundryStorageProvider into the shared core so protocol packages can build resource-specific clients on top.
Changes:
- New
storagesubpackage: client/transport, endpoint resolution, error hierarchy, pipeline policies, JSON helpers, and theFoundryStateStoreKV store with its serializer. - Added
azure-core>=1.30.0dependency and a CHANGELOG2.0.0b6 (Unreleased)entry. - Added unit tests for request construction and response handling.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
storage/__init__.py |
Public exports for the new storage package. |
storage/_client.py |
FoundryStorageClient base: pipeline, policies, _send_storage_request. |
storage/_endpoint.py |
FoundryStorageEndpoint resolution and versioned URL building. |
storage/_errors.py |
Storage exception hierarchy incl. new FoundryStoragePreconditionError (412). |
storage/_policies.py |
UA + per-retry logging policies with URL masking. |
storage/_state.py |
FoundryStateStore read/write/list_keys/get/set/delete. |
storage/_state_serializer.py |
Wire (de)serialization + StateItem/StateKey/KeyPage types. |
storage/_json.py |
Small JSON parsing helper. |
tests/test_foundry_state_store.py |
Unit tests for request/response behavior. |
pyproject.toml |
Adds azure-core>=1.30.0 dependency. |
CHANGELOG.md |
Adds 2.0.0b6 (Unreleased) feature entry (version not synced — see comment). |
13e374f to
1d7b980
Compare
…e-storage spec (foundrysdk_specs#247)
Per the latest commit on coreai-microsoft/foundrysdk_specs#247
("rename route to /storage/state_stores, add store PATCH update, align
object descriptors"), the state-store REST path is /storage/state_stores/*
(snake_case with underscore), not /storage/statestores/*.
- _state.py: store path + create() now target state_stores.
- _policies.py: masked-logging allowlist updated to the new segment name.
- Updated docs/state-store-guide.md, README, and test URL assertions.
- Fixed the core CHANGELOG/README, which still described the earlier
namespace-based design (pre-dating the PR Azure#47763 pull) instead of the
current store-bound statestores-protocol API; also dropped an unused
aiohttp dependency left over from that earlier design.
No functional change beyond the route rename -- the object-type descriptor
changes in the spec commit (state_store / state_store.item) are response-only
fields the SDK does not parse or assert on.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…te/get/update/delete Per review feedback on PR Azure#47763 (constructor-then-create felt awkward) and the guide-first workflow: reshape the store-admin surface to four verbs instead of six, and stop threading the per-request delegated-user header through the constructor. - FoundryStateStore.get_or_create(name, ...) is now the sole entry point (an async classmethod): resolves the store in one call (fetch, or create on first use, refetching on a create/create race), replacing the previous constructor + separate create()/create_or_get()/get_or_create() dance. - get(key=None) and delete(key=None, ...) are overloaded on whether a key is supplied: no key acts on the bound store itself (was get_properties / delete_store); a key acts on one item (unchanged item-level behavior). update(...) replaces update_metadata(...) (no collision, simple rename). - Removed the constructor's user_id parameter. x-ms-user-id is a per-request delegation header, not a store-level setting -- it is now resolved dynamically, per call, from azure.ai.agentserver.core's existing request-scoped platform context (get_request_context().user_id), the same mechanism protocol hosts already populate from the inbound x-agent-user-id header. A single (possibly long-lived, reused) FoundryStateStore instance can now safely serve requests for different users. - azure-ai-agentserver-activity's FoundryStorage updated to call the new classmethod for writes while keeping the plain constructor for reads/ deletes (both already tolerate a not-yet-created store gracefully), so the "only create on first write" behavior is unchanged. - Rewrote the core state-store tests, the sample, and the developer guide for the new shape; added a "Limits" section to the guide mirroring the spec's field-constraints tables (no equivalent guide exists yet for the resilient-task primitive to model this section after). Verified: black, mypy, and pytest all clean for both packages (148 + 99 passed). Two mypy findings in _state.py (tags Union narrowing in update(), **query kwargs in list_keys) are pre-existing, inherited verbatim from PR Azure#47763's pulled code -- unrelated to this rename and left as-is. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tate-store guide Same fix as the sibling PR Azure#47763: the guide's code samples returned typed objects (StateStore, StateStoreItem, etc.) but never imported or named the types, so every example read as if get()/set()/list_keys() returned raw dicts. Added a Typed Models section mapping each method to its return model, and added explicit imports/type annotations to the Getting Started, Store Lifecycle, Fetch-one-item, and Listing Keys examples. StateStoreItem.value stays intentionally untyped (opaque application JSON) -- called that out explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review feedback (PR Azure#47763): remove the key-vs-no-key @Overloads on FoundryStateStore.get/delete and give every item operation an explicit, consistently named method. - set -> set_item - get(key) -> get_item(key); bare get() stays store-scoped - delete(key) -> delete_item(key); bare delete() stays store-scoped - create_item / list_keys unchanged Store-level get()/update()/delete() now unambiguously act on the bound store. Updates README, state-store guide, samples, tests, and CHANGELOG to match. No TypeSpec/generated-model changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fied with Azure#47763) Bring the PR Azure#47763 review fix (explicit, consistently named item operations) into the unified activity+storage branch and update the activity bridge to match. Core (FoundryStateStore): - remove key-vs-no-key @Overloads on get/delete - set -> set_item, get(key) -> get_item, delete(key) -> delete_item - bare get()/update()/delete() stay store-scoped - create_item / list_keys unchanged - keep unified's call-id identity model (x-agent-foundry-call-id); no x-ms-user-id/user_id delegation Activity bridge (_foundry_storage.py): - _read_item/_write_item/_delete_item now call get_item/set_item/delete_item - update fake-store mocks + assertions in test_foundry_storage.py Docs/samples/tests updated. No TypeSpec/generated-model changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5d04c21 to
3816b39
Compare
| store itself; the explicit `*_item` methods act on individual items within it. | ||
|
|
||
| ```python | ||
| info: StateStore | None = await store.get() # the store's metadata, or None if absent |
There was a problem hiding this comment.
Suggestion: Returning None on a .get() is a bit ambiguous here, is the store missing vs is the store metadata missing.
if the store exists, I'd expect store.get() to always return the store object/metadata instead of None.
If this needs to return a None a different method name like .get_info() could be better.
There was a problem hiding this comment.
ok this is the sample customer code its not that we give None if the store doesn't exist we generally give a 404 but this is more of a guidance that we are giving to customer on how to use it
There was a problem hiding this comment.
I understand that the API returns 404 and I see that we capture that 404 and return a None
My concern is more around the method semantics than the transport behavior.
get() feels like a more natural fit as a static/factory-style API (e.g. FoundryStateStore.get(name)), where the resource may or may not exist.
Here we already have an FoundryStateStore instance bound to a particular store, with instance do we expect the API to ever return a 404 ?
if yes, we might have to rethink if .get() is the right name here.
There was a problem hiding this comment.
yes on the first draft of the pr the name was getStateStoreMetadata() it was changed because of the feedback once you say stateStoreMetadata you have to return a different typed object other than StateStore itself
There was a problem hiding this comment.
Just saw some old comments, I think ravi's comment is also along the same lines
store = await FoundryStateStore.get(name); is what he mentioned in comments, which is a factory style object.
our current implementation does not match this.
| :return: The updated store descriptor. | ||
| :rtype: ~azure.ai.agentserver.core.storage.StateStore | ||
| """ | ||
| body = serialize_store_update_request(description, tags) |
There was a problem hiding this comment.
Done. _state.py now imports a single shared _UNSET sentinel from _state_serializer, so omitted args are correctly dropped. Added regression tests for update(description=...), update(tags=...), and update().
| try: | ||
| await store._fetch_properties() | ||
| except FoundryStorageNotFoundError: | ||
| try: | ||
| await store._create_properties() | ||
| except FoundryStorageConflictError: | ||
| await store._fetch_properties() | ||
| return store |
There was a problem hiding this comment.
Done. get_or_create now wraps fetch/create/refetch in try/except BaseException and calls store.aclose() before re-raising, so the pipeline and any owned credential are closed on every failure (including cancellation). Added tests covering fetch and create failures.
| StateStoreItemKey, | ||
| ) | ||
|
|
||
| __all__ = [ |
There was a problem hiding this comment.
Done. Regenerated and committed api.md and api.metadata.yml; they now include the full azure.ai.agentserver.core.storage surface.
| def _store_path(self) -> str: | ||
| return f"state_stores/{_encode_segment(self._name)}" |
There was a problem hiding this comment.
Done. Updated the PR description to document the actual state_stores/{name}/items wire contract implemented here.
Add azure.ai.agentserver.core.storage: protocol-neutral FoundryStorageClient (transport, endpoint, pipeline policies, error hierarchy) plus FoundryStateStore, a durable key-value store bound to one explicit store. get_or_create(name, ...) is the primary entry point; store-level get/update/delete act on the bound store, and explicit create_item/set_item/get_item/delete_item/list_keys act on items. Addresses PR review feedback: - Share a single _UNSET sentinel between _state and _state_serializer so one-field update() calls no longer leak the sentinel into the payload. - Close the store (pipeline + owned credential) on every get_or_create failure, including cancellation, so a failed resolution cannot leak resources. - Bound-store get() returns StateStore and raises FoundryStorageNotFoundError instead of returning None; docs and tests updated to match. - Regenerate api.md / api.metadata.yml to include the storage surface. - Add regression tests for one-field updates and get_or_create cleanup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2c9dca80-df28-4b1e-8c61-3a69bf7e9a8a
d6eba57 to
d80ee45
Compare
| @@ -1,205 +1,371 @@ | |||
| ```py | |||
| namespace azure.ai.agentserver.core | |||
| namespace azure.ai.agentserver.core.storage | |||
| """Build a full storage API URL for *path* with ``api-version`` appended.""" | ||
| url = f"{self.storage_base_url}{path}?api-version={_encode(self.api_version)}" | ||
| for key, value in extra_params.items(): | ||
| url += f"&{key}={_encode(value)}" |
| policies.RequestIdPolicy(), | ||
| policies.HeadersPolicy(), | ||
| ua_policy, | ||
| policies.AsyncRetryPolicy(), |
| # Endpoint and credential resolve from FOUNDRY_PROJECT_ENDPOINT + DefaultAzureCredential. | ||
| # get_or_create() resolves (or creates, on first use) the store in one call. | ||
| store = await FoundryStateStore.get_or_create("checkpoints/thread-abc", user_isolation=True) |
| __all__ = [ | ||
| "DEFAULT_ITEM_TTL_SECONDS", | ||
| "DeletedStateStore", | ||
| "DeletedStateStoreItem", | ||
| "FOUNDRY_TOKEN_SCOPE", |
| async def __aenter__(self) -> "FoundryStorageClient": | ||
| return self |
| if not (endpoint.startswith("http://") or endpoint.startswith("https://")): | ||
| raise ValueError(f"endpoint must be a valid absolute URL, got: {endpoint!r}") | ||
| normalized = endpoint.rstrip("/") | ||
| if normalized.endswith("/storage"): | ||
| storage_base_url = normalized + "/" | ||
| else: | ||
| storage_base_url = normalized + "/storage/" |
[Pilot] PR Pipeline Failure AnalysisA CI pipeline failed on this pull request. Here is an automated analysis of what went wrong and how to get the build green. What failedTwo distinct failure categories were detected in the
Recommended next steps
Raw pipeline analysis (azsdk ci analyze)
|
Add azure.ai.agentserver.core.storage: protocol-neutral FoundryStorageClient (transport, endpoint, pipeline policies, error hierarchy) plus FoundryStateStore, a durable key-value store bound to one explicit store.
FoundryStateStore.get_or_create(name, ...)is the primary entry point; store-level operations (get/update/delete) act on the bound store, and explicit item operations (create_item/set_item/get_item/delete_item/list_keys) act on items within it. On the wire, store and item names are base64url path-encoded:POST /storage/state_stores,GET|PATCH|DELETE /storage/state_stores/{name},POST /storage/state_stores/{name}/items,GET|PUT|DELETE /storage/state_stores/{name}/items/{key}, andGET /storage/state_stores/{name}/items:keysfor ordered paged key listing, with optionalif_matchoptimistic concurrency. Adds azure-core dependency and tests.Responses has its own storage providers idea is to make responses re-use this storage clients in a follow-up PR
Description
Please add an informative description that covers that changes made by the pull request and link all relevant issues.
If an SDK is being regenerated based on a new API spec, a link to the pull request containing these API spec changes should be included above.
All SDK Contribution checklist:
General Guidelines and Best Practices
Testing Guidelines