agent-framework-hosting 1.0.0a260709__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Execution-state helpers for app-owned Agent Framework hosting routes."""
4
+
5
+ import importlib.metadata
6
+
7
+ from ._state import (
8
+ AgentRunArgs,
9
+ AgentState,
10
+ SessionStore,
11
+ SupportsBuild,
12
+ WorkflowRunArgs,
13
+ WorkflowState,
14
+ )
15
+
16
+ try:
17
+ __version__ = importlib.metadata.version(__name__)
18
+ except importlib.metadata.PackageNotFoundError:
19
+ __version__ = "0.0.0"
20
+
21
+ __all__ = [
22
+ "AgentRunArgs",
23
+ "AgentState",
24
+ "SessionStore",
25
+ "SupportsBuild",
26
+ "WorkflowRunArgs",
27
+ "WorkflowState",
28
+ "__version__",
29
+ ]
@@ -0,0 +1,338 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ """Shared execution state for app-owned hosting routes.
4
+
5
+ Two independent state holders, one per target kind, since agents and
6
+ workflows keep different continuation state:
7
+
8
+ - ``AgentState`` pairs an agent target with a ``SessionStore``
9
+ (``session_id -> AgentSession``).
10
+ - ``WorkflowState`` resolves a workflow target. Workflow checkpointing uses
11
+ the existing ``CheckpointStorage`` abstraction directly; apps can keep their
12
+ own ``session_id -> checkpoint_id`` cursor when they need per-session resume.
13
+
14
+ ``SessionStore`` is plain storage: it only gets/sets/deletes what it is given.
15
+ It does not know how to create a new value for a ``session_id`` it hasn't seen
16
+ before -- that is ``AgentState``'s job (see ``AgentState.get_or_create_session``),
17
+ since only the state object has both the store and the resolved target.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import asyncio
23
+ import inspect
24
+ from collections.abc import Awaitable, Callable, Mapping
25
+ from typing import Any, Generic, Protocol, TypedDict, TypeVar, cast, runtime_checkable
26
+
27
+ from agent_framework import (
28
+ AgentRunInputs,
29
+ AgentSession,
30
+ ChatOptions,
31
+ SupportsAgentRun,
32
+ Workflow,
33
+ )
34
+
35
+
36
+ class SessionStore:
37
+ """Plain in-memory ``session_id -> AgentSession`` lookup.
38
+
39
+ This store only stores and retrieves; it does not create sessions. Use
40
+ :meth:`AgentState.get_or_create_session` for that -- it resolves the
41
+ agent target and calls ``target.create_session(...)`` the first time a
42
+ given ``session_id`` is seen, then stores the result here.
43
+
44
+ No eviction: every id ever stored stays resolvable for the life of the
45
+ process. That is intentional -- protocols such as OpenAI Responses'
46
+ ``previous_response_id`` are designed to let a caller continue from *any*
47
+ earlier point in a conversation, not just the latest turn, so every id
48
+ that has been handed out needs to stay independently resolvable. If you
49
+ back a ``SessionStore``-shaped store with real storage (Redis, a
50
+ database, ...), you are responsible for that store's own TTL/eviction
51
+ policy; this in-memory reference implementation does not model that
52
+ concern.
53
+ """
54
+
55
+ def __init__(self) -> None:
56
+ """Create an empty session store."""
57
+ self._sessions: dict[str, AgentSession] = {}
58
+
59
+ async def get(self, session_id: str) -> AgentSession | None:
60
+ """Return the stored session for ``session_id``, or ``None`` if absent.
61
+
62
+ Args:
63
+ session_id: Opaque app-selected session id.
64
+
65
+ Raises:
66
+ ValueError: If ``session_id`` is empty.
67
+ """
68
+ if not session_id:
69
+ raise ValueError("session_id must be a non-empty string")
70
+ return self._sessions.get(session_id)
71
+
72
+ async def set(self, session_id: str, session: AgentSession) -> None:
73
+ """Store ``session`` under ``session_id``, replacing any existing entry.
74
+
75
+ Args:
76
+ session_id: Opaque app-selected session id.
77
+ session: The session to store.
78
+
79
+ Raises:
80
+ ValueError: If ``session_id`` is empty.
81
+ """
82
+ if not session_id:
83
+ raise ValueError("session_id must be a non-empty string")
84
+ self._sessions[session_id] = session
85
+
86
+ async def delete(self, session_id: str) -> None:
87
+ """Forget the stored session for ``session_id``, if any.
88
+
89
+ Args:
90
+ session_id: Opaque app-selected session id.
91
+
92
+ Raises:
93
+ ValueError: If ``session_id`` is empty.
94
+ """
95
+ if not session_id:
96
+ raise ValueError("session_id must be a non-empty string")
97
+ self._sessions.pop(session_id, None)
98
+
99
+
100
+ AgentT = TypeVar("AgentT", bound=SupportsAgentRun)
101
+ WorkflowT = TypeVar("WorkflowT", bound=Workflow)
102
+
103
+
104
+ @runtime_checkable
105
+ class SupportsBuild(Protocol):
106
+ """A builder that produces a ``Workflow`` via a zero-argument ``build()``.
107
+
108
+ Matches ``agent_framework.WorkflowBuilder`` and the orchestration
109
+ builders in ``agent_framework_orchestrations`` (``ConcurrentBuilder``,
110
+ ``GroupChatBuilder``, ``HandoffBuilder``, ``MagenticBuilder``,
111
+ ``SequentialBuilder``) structurally, without ``agent-framework-hosting``
112
+ depending on either package.
113
+ """
114
+
115
+ def build(self) -> Workflow: ...
116
+
117
+
118
+ class AgentRunArgs(TypedDict):
119
+ """Arguments prepared for ``Agent.run``."""
120
+
121
+ messages: AgentRunInputs
122
+ options: ChatOptions[Any]
123
+ stream: bool
124
+
125
+
126
+ class WorkflowRunArgs(TypedDict):
127
+ """Arguments prepared for ``Workflow.run``."""
128
+
129
+ message: Any | None
130
+ responses: Mapping[str, Any] | None
131
+ stream: bool
132
+
133
+
134
+ class AgentState(Generic[AgentT]):
135
+ """Shared execution state for app-owned agent hosting routes.
136
+
137
+ Holds the Agent Framework agent target and a :class:`SessionStore` that
138
+ route code may share. Does not own routes, middleware, protocol
139
+ dispatch, or native SDK calls -- web frameworks keep those concerns.
140
+ """
141
+
142
+ def __init__(
143
+ self,
144
+ target: AgentT | Awaitable[AgentT] | Callable[[], AgentT | Awaitable[AgentT]],
145
+ *,
146
+ session_store: SessionStore | None = None,
147
+ cache_target: bool = True,
148
+ ) -> None:
149
+ """Create shared state for ``target``.
150
+
151
+ Args:
152
+ target: Agent target used by route code. May be a target
153
+ instance, a synchronous factory, an asynchronous factory, or
154
+ an awaitable target.
155
+
156
+ Keyword Args:
157
+ session_store: Existing store to use. Defaults to a fresh
158
+ in-memory :class:`SessionStore`.
159
+ cache_target: Whether to cache a resolved callable/awaitable
160
+ target. Defaults to ``True`` so expensive target setup
161
+ happens once.
162
+
163
+ Raises:
164
+ ValueError: If ``cache_target=False`` is used with a one-shot
165
+ awaitable target.
166
+ """
167
+ if not cache_target and inspect.isawaitable(target):
168
+ raise ValueError("cache_target=False requires a target instance or callable target factory")
169
+ self._target_source = target
170
+ self._cache_target = cache_target
171
+ self._cached_target: AgentT | None = None
172
+ self._target_lock = asyncio.Lock()
173
+ if not callable(target) and not inspect.isawaitable(target):
174
+ self._cached_target = target
175
+ self._session_store: SessionStore = session_store if session_store is not None else SessionStore()
176
+ self._session_locks: dict[str, asyncio.Lock] = {}
177
+
178
+ async def get_target(self) -> AgentT:
179
+ """Return the resolved target.
180
+
181
+ Returns:
182
+ The target instance. Callable and awaitable targets are resolved
183
+ first and cached by default.
184
+ """
185
+ if self._cache_target and self._cached_target is not None:
186
+ return self._cached_target
187
+
188
+ if self._cache_target:
189
+ async with self._target_lock:
190
+ if self._cached_target is not None:
191
+ return self._cached_target
192
+ target = self._target_source() if callable(self._target_source) else self._target_source
193
+ if inspect.isawaitable(target):
194
+ target = await target
195
+ self._cached_target = target
196
+ return target
197
+
198
+ target = self._target_source() if callable(self._target_source) else self._target_source
199
+ if inspect.isawaitable(target):
200
+ target = await target
201
+ return target
202
+
203
+ @property
204
+ def target(self) -> AgentT:
205
+ """Return a synchronously available target.
206
+
207
+ Raises:
208
+ RuntimeError: If the target is a callable or awaitable that has not
209
+ been resolved with :meth:`get_target`.
210
+ """
211
+ if self._cached_target is not None:
212
+ return self._cached_target
213
+ if not callable(self._target_source) and not inspect.isawaitable(self._target_source):
214
+ return self._target_source
215
+ raise RuntimeError("target is resolved asynchronously; use `await state.get_target()`")
216
+
217
+ @property
218
+ def session_store(self) -> SessionStore:
219
+ """Return the session store for this state."""
220
+ return self._session_store
221
+
222
+ async def get_or_create_session(self, session_id: str) -> AgentSession:
223
+ """Return the session for ``session_id``, creating and storing one if missing.
224
+
225
+ Args:
226
+ session_id: Opaque app-selected session id.
227
+
228
+ Returns:
229
+ The stored or newly created ``AgentSession``.
230
+ """
231
+ if not session_id:
232
+ raise ValueError("session_id must be a non-empty string")
233
+ session_lock = self._session_locks.setdefault(session_id, asyncio.Lock())
234
+ async with session_lock:
235
+ session = await self._session_store.get(session_id)
236
+ if session is None:
237
+ target = await self.get_target()
238
+ session = target.create_session(session_id=session_id)
239
+ await self._session_store.set(session_id, session)
240
+ return session
241
+
242
+ async def set_session(self, session_id: str, session: AgentSession) -> None:
243
+ """Store ``session`` under ``session_id`` in this state's session store.
244
+
245
+ Args:
246
+ session_id: Opaque app-selected session id.
247
+ session: Session to store.
248
+ """
249
+ await self._session_store.set(session_id, session)
250
+
251
+
252
+ class WorkflowState(Generic[WorkflowT]):
253
+ """Shared execution state for app-owned workflow hosting routes.
254
+
255
+ Holds the Agent Framework workflow target. Does not own routes,
256
+ middleware, protocol dispatch, native SDK calls, or checkpoint storage --
257
+ web frameworks and workflow/app code keep those concerns.
258
+ """
259
+
260
+ def __init__(
261
+ self,
262
+ target: WorkflowT | SupportsBuild | Awaitable[WorkflowT] | Callable[[], WorkflowT | Awaitable[WorkflowT]],
263
+ *,
264
+ cache_target: bool = True,
265
+ ) -> None:
266
+ """Create shared state for ``target``.
267
+
268
+ Args:
269
+ target: Workflow target used by route code. May be a target
270
+ instance, a ``WorkflowBuilder``-shaped builder (see
271
+ :class:`SupportsBuild`; the state calls ``build()`` for you),
272
+ a synchronous factory, an asynchronous factory, or an
273
+ awaitable target.
274
+
275
+ Keyword Args:
276
+ cache_target: Whether to cache a resolved callable/awaitable/built
277
+ target. Defaults to ``True`` so expensive target setup
278
+ happens once.
279
+
280
+ Raises:
281
+ ValueError: If ``cache_target=False`` is used with a one-shot
282
+ awaitable target.
283
+ """
284
+ if isinstance(target, SupportsBuild):
285
+ # WorkflowBuilder (and the orchestration builders) are not
286
+ # themselves callable or awaitable, so normalize to the bound
287
+ # `build` method -- the resolution logic below already knows how
288
+ # to treat a zero-arg factory. `build()` is typed to return the
289
+ # `Workflow` base class rather than this instance's narrower
290
+ # `WorkflowT`, but it is the same object the caller asked for.
291
+ target = cast("Callable[[], WorkflowT]", target.build)
292
+ if not cache_target and inspect.isawaitable(target):
293
+ raise ValueError("cache_target=False requires a target instance or callable target factory")
294
+ self._target_source = target
295
+ self._cache_target = cache_target
296
+ self._cached_target: WorkflowT | None = None
297
+ self._target_lock = asyncio.Lock()
298
+ if not callable(target) and not inspect.isawaitable(target):
299
+ self._cached_target = target
300
+
301
+ async def get_target(self) -> WorkflowT:
302
+ """Return the resolved target.
303
+
304
+ Returns:
305
+ The target instance. Callable and awaitable targets are resolved
306
+ first and cached by default.
307
+ """
308
+ if self._cache_target and self._cached_target is not None:
309
+ return self._cached_target
310
+
311
+ if self._cache_target:
312
+ async with self._target_lock:
313
+ if self._cached_target is not None:
314
+ return self._cached_target
315
+ target = self._target_source() if callable(self._target_source) else self._target_source
316
+ if inspect.isawaitable(target):
317
+ target = await target
318
+ self._cached_target = target
319
+ return target
320
+
321
+ target = self._target_source() if callable(self._target_source) else self._target_source
322
+ if inspect.isawaitable(target):
323
+ target = await target
324
+ return target
325
+
326
+ @property
327
+ def target(self) -> WorkflowT:
328
+ """Return a synchronously available target.
329
+
330
+ Raises:
331
+ RuntimeError: If the target is a callable or awaitable that has not
332
+ been resolved with :meth:`get_target`.
333
+ """
334
+ if self._cached_target is not None:
335
+ return self._cached_target
336
+ if not callable(self._target_source) and not inspect.isawaitable(self._target_source):
337
+ return self._target_source
338
+ raise RuntimeError("target is resolved asynchronously; use `await state.get_target()`")
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-framework-hosting
3
+ Version: 1.0.0a260709
4
+ Summary: Execution-state helpers for app-owned Agent Framework hosting routes.
5
+ Author-email: Microsoft <af-support@microsoft.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ License-File: LICENSE
19
+ Requires-Dist: agent-framework-core>=1.11.0,<2
20
+ Project-URL: homepage, https://aka.ms/agent-framework
21
+ Project-URL: issues, https://github.com/microsoft/agent-framework/issues
22
+ Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
23
+ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
24
+
25
+ # agent-framework-hosting
26
+
27
+ Shared execution-state helpers for app-owned Agent Framework hosting.
28
+
29
+ This package keeps Agent Framework state separate from web-framework concerns:
30
+
31
+ - `AgentState` — pairs an agent target with a `SessionStore`
32
+ (`session_id -> AgentSession`).
33
+ - `WorkflowState` — resolves a workflow target, including direct `Workflow`
34
+ instances, workflow factories, `WorkflowBuilder`, and orchestration builders.
35
+
36
+ `SessionStore` is plain storage: `get`/`set`/`delete` by an app-selected id,
37
+ nothing more. It does not know how to create a new value for an id it hasn't
38
+ seen before — use `AgentState.get_or_create_session(...)` for that, since only
39
+ the state object has both the store and the resolved target. Workflow
40
+ checkpointing should use the existing `CheckpointStorage` abstraction directly;
41
+ if an app needs per-session resume, keep a small app-owned cursor such as
42
+ `session_id -> checkpoint_id`.
43
+
44
+ Use FastAPI, Starlette, Azure Functions, Django, or another framework for route
45
+ registration, auth, middleware, response construction, and background work.
46
+
47
+ > The built-in `SessionStore` is an in-memory `dict` with no eviction — every
48
+ > id ever stored stays resolvable for the life of the process. That is
49
+ > intentional: protocols such as OpenAI Responses'
50
+ > `previous_response_id` are designed to let a caller continue from *any*
51
+ > earlier point in a conversation, not just the latest turn, so every id
52
+ > handed out needs to stay independently resolvable. If you back the store
53
+ > with real storage (Redis, a database, ...), you are responsible for that
54
+ > store's own TTL/eviction policy; this in-memory reference implementation
55
+ > does not model that concern.
56
+
57
+ ## Quickstart
58
+
59
+ ```python
60
+ from agent_framework.openai import OpenAIChatClient
61
+ from agent_framework_hosting import AgentState
62
+
63
+ agent = OpenAIChatClient().as_agent(name="Assistant")
64
+ state = AgentState(agent)
65
+
66
+ session = await state.get_or_create_session("conversation-1")
67
+ result = await (await state.get_target()).run("Hello", session=session)
68
+ ```
69
+
70
+ If a protocol mints a new continuation id on every response, store the session
71
+ explicitly after `run(...)` returns. `run(...)` may update the session, so store
72
+ the post-run object:
73
+
74
+ ```python
75
+ session = await state.get_or_create_session(previous_response_id)
76
+ result = await (await state.get_target()).run("Hello", session=session)
77
+ await state.set_session(response_id, session)
78
+ ```
79
+
80
+ Targets can be direct instances, synchronous factories, asynchronous factories,
81
+ or awaitables:
82
+
83
+ ```python
84
+ state = AgentState(create_agent) # cached by default
85
+ state = AgentState(create_agent, cache_target=False)
86
+ ```
87
+
88
+ `WorkflowState` mirrors this shape for workflow targets:
89
+
90
+ ```python
91
+ from agent_framework import InMemoryCheckpointStorage
92
+ from agent_framework_hosting import WorkflowState
93
+
94
+ state = WorkflowState(create_workflow)
95
+ storage = InMemoryCheckpointStorage()
96
+ result = await (await state.get_target()).run("Hello", checkpoint_storage=storage)
97
+ latest = await storage.get_latest(workflow_name=(await state.get_target()).name)
98
+ ```
99
+
100
+ `WorkflowState` also accepts an unbuilt workflow builder directly:
101
+
102
+ ```python
103
+ from agent_framework import WorkflowBuilder
104
+ from agent_framework_hosting import WorkflowState
105
+
106
+ builder = WorkflowBuilder(start_executor=executor)
107
+ state = WorkflowState(builder) # calls builder.build() when the target is resolved
108
+ ```
109
+
110
+ This is structural: orchestration builders from `agent_framework_orchestrations`
111
+ (`SequentialBuilder`, `ConcurrentBuilder`, `HandoffBuilder`, `GroupChatBuilder`,
112
+ and `MagenticBuilder`) also work because they expose the same zero-argument
113
+ `build() -> Workflow` method.
114
+
115
+ Cross-channel identity linking, multicast delivery, background runs,
116
+ continuation tokens, and durable delivery runners are follow-up enhancements,
117
+ not part of this v1 state surface.
118
+
@@ -0,0 +1,6 @@
1
+ agent_framework_hosting/__init__.py,sha256=cku2Rb9iNbkg5nR3w6VK2o6CPpvFSKhPYzeYGk-o6Aw,580
2
+ agent_framework_hosting/_state.py,sha256=X6Iun9o8PCPQZ8B54rL0YGTz2KMVUYmVkwtY9Zn_cyw,13185
3
+ agent_framework_hosting-1.0.0a260709.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
4
+ agent_framework_hosting-1.0.0a260709.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
5
+ agent_framework_hosting-1.0.0a260709.dist-info/METADATA,sha256=5kQBPfLvi2FE33kFbpt1YQLbylOsPDQFsBfhOpSmnUc,4947
6
+ agent_framework_hosting-1.0.0a260709.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE