From 6fd12205a36695edf0583d807422a7f29d90cc79 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 09:40:07 +0000 Subject: [PATCH 01/22] docs(microsoft-agent-framework): 10-API deep dives Vol. 5 for 1.19.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Class Deep Dives Vol. 5, source-verified against agent-framework 1.19.0 (latest PyPI release). Covers 10 previously undocumented public APIs spanning a broad range of the framework: 1. WorkflowEvent[DataT] — unified event bus: all lifecycle, diagnostic, bookkeeping, and data-plane events via typed factory methods, property accessors for request_info, and JSON serialization round-trip. 2. AgentContext — mutable context through the agent middleware pipeline; demonstrates timing middleware, message injection, and result override. 3. MiddlewareBundle — indivisible middleware group for features that only uphold their contract when installed together (e.g. agent-hooks). 4. ConversationSplit / ConversationSplitter — built-in LAST_TURN/FULL enum strategies plus the structural Protocol for custom splitters. 5. VectorStoreHistoryProvider — vector-backed conversation history with multi-dimensional scoping, optional embeddings, compaction, and a semantic search_history tool. 6. MemoryStore (ABC) / MemoryFileStore — abstract backing store interface for the memory harness; concrete filesystem implementation; custom in-memory implementation example for testing. 7. MemoryTopicRecord — topic memory file: constructor, markdown round-trip, to_dict/from_dict, and search helpers. 8. WorkflowRunResult — list[WorkflowEvent] subclass; get_outputs(), get_final_state(), status_timeline(), and pending request detection. 9. FunctionInvocationContext — function middleware context including progressive tool exposure (add_tools / remove_tools) with all-or-nothing batch semantics; argument sanitisation middleware example. 10. ChatOptions — TypedDict of cross-provider request parameters; default_options, per-run overrides, structured output, and Unpack[ChatOptions] type-safe forwarding. Also updates the comprehensive guide to reference Vol. 5 and bumps the latest version badge to 1.19.0. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 1421 +++++++++++++++++ ...nt_framework_python_comprehensive_guide.md | 8 +- 2 files changed, 1425 insertions(+), 4 deletions(-) create mode 100644 src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md new file mode 100644 index 00000000..87d9954a --- /dev/null +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -0,0 +1,1421 @@ +--- +title: "Microsoft Agent Framework (Python) — 10-API Deep Dives Vol. 5 (1.19.0)" +description: "Source-verified deep dives for WorkflowEvent, AgentContext, MiddlewareBundle, ConversationSplit/ConversationSplitter, VectorStoreHistoryProvider, MemoryStore/MemoryFileStore, MemoryTopicRecord, WorkflowRunResult, FunctionInvocationContext, and ChatOptions — all verified against agent-framework 1.19.0 source." +framework: microsoft-agent-framework +language: python +--- + +# agent-framework (Python) — 10-API Deep Dives Vol. 5 + +**Verified against:** `agent-framework==1.19.0` +**Python requirement:** 3.10+ + +This volume covers 10 additional public APIs spanning the workflow event bus, agent and function middleware contexts, indivisible middleware bundles, evaluation conversation splitting, vector-store-backed history, topic-based memory stores, workflow run results, progressive tool exposure, and the cross-provider `ChatOptions` TypedDict. Each section includes the full constructor or signature, every meaningful method or factory, and self-contained runnable examples verified against the 1.19.0 source. + +See [Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/) for `WorkflowViz`, `FileMemoryProvider`, `AgentModeProvider`, `BackgroundAgentsProvider`, `ToolApprovalMiddleware`, `SwitchCaseEdgeGroup`, `MessageInjectionMiddleware`, `ToolResultCompactionStrategy`, `SummarizationStrategy`, and `TokenBudgetComposedStrategy`. + +See [Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/) for `FanInEdgeGroup`, `FanOutEdgeGroup`, `FunctionalWorkflow`, `FunctionalWorkflowAgent`, `FileCheckpointStorage`, `InMemoryCheckpointStorage`, `MCPStdioTool`, `MCPStreamableHTTPTool`, `SelectiveToolCallCompactionStrategy`, and `TodoProvider`. + +See [Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/) for `WorkflowBuilder`, `SlidingWindowStrategy`, `TruncationStrategy`, `ContextWindowCompactionStrategy`, `LocalEvaluator`, `InlineSkill`, `FileAccessProvider`, `MemoryContextProvider`, `FileHistoryProvider`, and `MCPWebsocketTool`. + +See [Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/) for `VectorStoreField`, `VectorStoreCollectionDefinition`, `InMemoryCollection`, `InMemoryStore`, `Filter`, `FilterGroup`, `SecretString`, `load_settings`, `create_agent_hooks_middleware`, and `GroupChatBuilder`. + +--- + +## 1. `WorkflowEvent[DataT]` + +**Module:** `agent_framework._workflows._events` (re-exported via `agent_framework`) + +`WorkflowEvent` is the single generic class for all events emitted during a workflow run. Every emission carries a `type` discriminator string (lifecycle, diagnostic, data, or bookkeeping) and an optional typed `data` payload. The framework emits these automatically; application code consumes them from `WorkflowRunResult` or from a streaming `async for` loop. + +> **Note on `WorkflowEvent.emit()`:** The `emit()` factory is deprecated since 1.14.0. Use `ctx.yield_output()` from an intermediate-designated executor instead. + +### Constructor + +```python +WorkflowEvent( + type: WorkflowEventType, # discriminator string — see table below + data: DataT | None = None, + *, + origin: WorkflowEventSource | None = None, + state: WorkflowRunState | None = None, # STATUS events + details: WorkflowErrorDetails | None = None, # FAILED events + executor_id: str | None = None, # OUTPUT / DATA / executor events + request_id: str | None = None, # REQUEST_INFO events + source_executor_id: str | None = None, # REQUEST_INFO events + request_type: type[Any] | None = None, # REQUEST_INFO events + response_type: type[Any] | None = None, # REQUEST_INFO events + iteration: int | None = None, # SUPERSTEP events +) +``` + +> Prefer the factory methods over the constructor directly. + +### Event type table + +| `type` string | Factory method | `data` type | Key extra fields | +|---|---|---|---| +| `"started"` | `WorkflowEvent.started()` | `None` / DataT | — | +| `"status"` | `WorkflowEvent.status(state)` | `None` / DataT | `state` | +| `"failed"` | `WorkflowEvent.failed(details)` | `None` / DataT | `details` | +| `"warning"` | `WorkflowEvent.warning(msg)` | `str` | — | +| `"error"` | `WorkflowEvent.error(exc)` | `Exception` | — | +| `"output"` | emitted by `ctx.yield_output()` | DataT | `executor_id` | +| `"intermediate"` | emitted by `ctx.yield_output()` (intermediate) | DataT | `executor_id` | +| `"request_info"` | `WorkflowEvent.request_info(...)` | DataT | `request_id`, `source_executor_id` | +| `"superstep_started"` | `WorkflowEvent.superstep_started(n)` | `None` / DataT | `iteration` | +| `"superstep_completed"` | `WorkflowEvent.superstep_completed(n)` | `None` / DataT | `iteration` | +| `"executor_invoked"` | `WorkflowEvent.executor_invoked(id)` | `None` / DataT | `executor_id` | +| `"executor_completed"` | `WorkflowEvent.executor_completed(id)` | `None` / DataT | `executor_id` | +| `"executor_failed"` | `WorkflowEvent.executor_failed(id, details)` | `WorkflowErrorDetails` | `executor_id`, `details` | +| `"executor_bypassed"` | `WorkflowEvent.executor_bypassed(id)` | `None` / DataT | `executor_id` — cache-hit replay | + +### Factory methods (classmethod) + +| Method | Signature | Notes | +|---|---|---| +| `started` | `(data=None) → WorkflowEvent[DataT]` | First event of every run | +| `status` | `(state: WorkflowRunState, data=None) → WorkflowEvent[DataT]` | State transitions | +| `failed` | `(details: WorkflowErrorDetails, data=None) → WorkflowEvent[DataT]` | Run termination | +| `warning` | `(message: str) → WorkflowEvent[str]` | User-emitted diagnostic | +| `error` | `(exception: Exception) → WorkflowEvent[Exception]` | User-emitted diagnostic | +| `request_info` | `(request_id, source_executor_id, request_data, response_type) → WorkflowEvent[DataT]` | Human-in-the-loop pause | +| `superstep_started` | `(iteration: int, data=None) → WorkflowEvent[DataT]` | Pregel superstep begin | +| `superstep_completed` | `(iteration: int, data=None) → WorkflowEvent[DataT]` | Pregel superstep end | +| `executor_invoked` | `(executor_id: str, data=None) → WorkflowEvent[DataT]` | Bookkeeping | +| `executor_completed` | `(executor_id: str, data=None) → WorkflowEvent[DataT]` | Bookkeeping | +| `executor_failed` | `(executor_id: str, details: WorkflowErrorDetails) → WorkflowEvent[WorkflowErrorDetails]` | Bookkeeping | +| `executor_bypassed` | `(executor_id: str, data=None) → WorkflowEvent[DataT]` | Cache replay | + +### Type-safe property accessors + +`request_id`, `source_executor_id`, `request_type`, `response_type` — each raises `RuntimeError` when accessed on an event whose `type` is not `"request_info"`. + +### Serialization + +```python +event.to_dict() → dict[str, Any] # only for "request_info" events +WorkflowEvent.from_dict(data, allowed_types=None) → WorkflowEvent[Any] +``` + +### Example — consuming events from a non-streaming run + +```python +import asyncio +from agent_framework import ( + Agent, WorkflowEvent, WorkflowBuilder, WorkflowRunState +) +from agent_framework.openai import OpenAIChatClient + +async def main(): + client = OpenAIChatClient() + agent = Agent(client=client, name="summarizer", + instructions="Summarize the user's text in one sentence.") + + workflow = WorkflowBuilder().add_agent(agent).build() + result = await workflow.run("The quick brown fox jumps over the lazy dog.") + + for event in result: + if event.type == "output": + print(f"Output from {event.executor_id}: {event.data}") + elif event.type == "status": + print(f"State → {event.state.value}") + elif event.type == "failed": + print(f"FAILED: {event.details}") + + final = result.get_final_state() + assert final == WorkflowRunState.IDLE + +asyncio.run(main()) +``` + +### Example — streaming events + +```python +import asyncio +from agent_framework import Agent, WorkflowBuilder +from agent_framework.openai import OpenAIChatClient + +async def stream(): + agent = Agent(client=OpenAIChatClient(), name="poet", + instructions="Write a haiku.") + workflow = WorkflowBuilder().add_agent(agent).build() + + async for event in workflow.stream("Cherry blossoms fall"): + match event.type: + case "started": + print("Workflow started") + case "superstep_started": + print(f" Superstep {event.iteration} begin") + case "executor_invoked": + print(f" Executor '{event.executor_id}' invoked") + case "output": + print(f" Output: {event.data}") + case "superstep_completed": + print(f" Superstep {event.iteration} done") + case "status": + print(f" State: {event.state.value}") + +asyncio.run(stream()) +``` + +### Example — human-in-the-loop via `request_info` + +```python +import asyncio +from agent_framework import WorkflowEvent + +# Pause-and-resume pattern: the executor emits a request_info event, +# the host reads it, supplies the answer, then resumes the workflow. +async def handle_pending_request(run_result, workflow): + pending = run_result.get_request_info_events() + if not pending: + return + + req: WorkflowEvent = pending[0] + # req.request_id, req.source_executor_id, req.data, req.response_type + print(f"Workflow is asking: {req.data}") + user_answer = input("Your answer: ") + + resumed = await workflow.respond( + request_id=req.request_id, + response=user_answer, + ) + outputs = resumed.get_outputs() + print(f"Final output: {outputs}") +``` + +--- + +## 2. `AgentContext` + +**Module:** `agent_framework._middleware` (re-exported via `agent_framework`) + +`AgentContext` is the mutable context object passed through the **agent middleware** pipeline on every `agent.run()` call. Middleware reads it before calling `call_next()` (to inspect or mutate the incoming request) and reads it again after (to inspect or replace the result). + +### Constructor + +```python +AgentContext( + *, + agent: SupportsAgentRun, + messages: list[Message], + session: AgentSession | None = None, + tools: ToolTypes | Callable | Sequence[...] | None = None, + options: Mapping[str, Any] | None = None, + stream: bool = False, + compaction_strategy: CompactionStrategy | None = None, + tokenizer: TokenizerProtocol | None = None, + metadata: Mapping[str, Any] | None = None, + result: AgentResponse | ResponseStream | None = None, + kwargs: Mapping[str, Any] | None = None, + client_kwargs: Mapping[str, Any] | None = None, + function_invocation_kwargs: Mapping[str, Any] | None = None, + stream_transform_hooks: Sequence[Callable] | None = None, + stream_result_hooks: Sequence[Callable] | None = None, + stream_cleanup_hooks: Sequence[Callable] | None = None, +) +``` + +> The framework constructs `AgentContext` for you. You receive it as the first argument of `AgentMiddleware.process()`. + +### Attributes + +| Attribute | Type | Notes | +|---|---|---| +| `agent` | `SupportsAgentRun` | The agent being invoked. Read-only in practice. | +| `messages` | `list[Message]` | Messages sent to the agent. Mutate to inject/remove messages before the call. | +| `session` | `AgentSession \| None` | The current session, or `None` for stateless runs. | +| `tools` | tool types | Run-level tool overrides. `None` → agent's declared tools apply. | +| `options` | `dict[str, Any]` | Merged run options (model, temperature, etc.). | +| `stream` | `bool` | `True` for streaming invocations. | +| `compaction_strategy` | `CompactionStrategy \| None` | Per-run compaction override. | +| `tokenizer` | `TokenizerProtocol \| None` | Per-run tokenizer override. | +| `metadata` | `dict[str, Any]` | Shared scratchpad for passing data between middleware layers. | +| `result` | `AgentResponse \| ResponseStream \| None` | Set after `call_next()`. Replace to override the agent's response. | +| `kwargs` | `dict[str, Any]` | Legacy runtime keyword arguments. | +| `client_kwargs` | `dict[str, Any]` | Client-specific kwargs forwarded to the underlying chat client. | +| `function_invocation_kwargs` | `dict[str, Any]` | Kwargs forwarded into every tool invocation on this run. | +| `stream_transform_hooks` | `list[Callable]` | Per-update streaming transformers. | +| `stream_result_hooks` | `list[Callable]` | Transformers applied to the final streaming result. | +| `stream_cleanup_hooks` | `list[Callable]` | Cleanup callbacks run after streaming completes. | + +### Example — timing middleware + +```python +import asyncio +import time +from agent_framework import Agent, AgentMiddleware, AgentContext +from agent_framework.openai import OpenAIChatClient + + +class TimingMiddleware(AgentMiddleware): + async def process(self, context: AgentContext, call_next): + start = time.perf_counter() + # Inspect what is being sent + print(f"Running agent: {context.agent.name!r}") + print(f"Messages: {len(context.messages)}") + print(f"Streaming: {context.stream}") + + context.metadata["start_time"] = start + await call_next() + + elapsed = time.perf_counter() - context.metadata["start_time"] + print(f"Elapsed: {elapsed:.3f}s") + if not context.stream: + print(f"Tokens used: {context.result.usage}") + + +async def main(): + agent = Agent( + client=OpenAIChatClient(), + name="demo", + instructions="Answer concisely.", + middleware=[TimingMiddleware()], + ) + result = await agent.run("What is 2 + 2?") + print(result.text) + +asyncio.run(main()) +``` + +### Example — injecting a system message + +```python +from agent_framework import Agent, AgentMiddleware, AgentContext, Message + + +class DateInjectorMiddleware(AgentMiddleware): + async def process(self, context: AgentContext, call_next): + import datetime + today = datetime.date.today().isoformat() + context.messages = [ + Message.from_system(f"Today's date is {today}."), + *context.messages, + ] + await call_next() +``` + +### Example — result override (mock testing) + +```python +from agent_framework import Agent, AgentMiddleware, AgentContext, AgentResponse, Message + + +class MockMiddleware(AgentMiddleware): + """Short-circuit the model call and return a canned response.""" + + def __init__(self, canned_text: str): + self._text = canned_text + + async def process(self, context: AgentContext, call_next): + # Skip call_next entirely — return canned response + context.result = AgentResponse( + message=Message.from_assistant(self._text), + ) +``` + +--- + +## 3. `MiddlewareBundle` + +**Module:** `agent_framework._middleware` (re-exported via `agent_framework`) + +> **Experimental:** requires `ExperimentalFeature.AGENT_HOOKS` to be acknowledged. + +A `MiddlewareBundle` groups several middleware objects into one opaque, indivisible unit. Features like `create_agent_hooks_middleware()` return a bundle because their internal middleware objects only uphold their contract when installed together — a bundle prevents accidental partial installation. + +Unlike a plain list, a `MiddlewareBundle` cannot be unpacked or sliced. Passing it to `Agent(middleware=[bundle, ...])` or `agent.run(middleware=[bundle, ...])` causes the framework to split its members into their agent/function/chat categories while preserving the ordering guarantee. + +### Constructor + +```python +MiddlewareBundle( + middleware: Sequence[ + AgentMiddleware | FunctionMiddleware | ChatMiddleware + | AgentMiddlewareCallable | FunctionMiddlewareCallable | ChatMiddlewareCallable + ] +) +``` + +**Raises:** +- `MiddlewareException` if a nested `MiddlewareBundle` is included (bundles cannot nest). +- `MiddlewareException` if any member's middleware category cannot be determined. + +### Example — creating and using a bundle + +```python +from agent_framework import ( + Agent, MiddlewareBundle, + AgentMiddleware, AgentContext, + FunctionMiddleware, FunctionInvocationContext, +) +from agent_framework.openai import OpenAIChatClient + + +class IngressMiddleware(AgentMiddleware): + async def process(self, context: AgentContext, call_next): + print("[Ingress] agent call started") + await call_next() + + +class EgressMiddleware(AgentMiddleware): + async def process(self, context: AgentContext, call_next): + await call_next() + print("[Egress] agent call completed") + + +class ToolAuditMiddleware(FunctionMiddleware): + async def process(self, context: FunctionInvocationContext, call_next): + print(f"[Audit] tool call: {context.function.name}") + await call_next() + + +# All three travel together as one indivisible bundle +enforcement_bundle = MiddlewareBundle([ + IngressMiddleware(), + EgressMiddleware(), + ToolAuditMiddleware(), +]) + +agent = Agent( + client=OpenAIChatClient(), + name="guarded", + middleware=[enforcement_bundle], +) +``` + +### Example — bundle returned by a factory (agent-hooks pattern) + +```python +import os +from agent_framework import Agent, acknowledge_experimental_feature, ExperimentalFeature +from agent_framework._harness._hooks import create_agent_hooks_middleware +from agent_framework.openai import OpenAIChatClient + +acknowledge_experimental_feature(ExperimentalFeature.AGENT_HOOKS) + +bundle = create_agent_hooks_middleware( + hooks_endpoint=os.environ["AGENT_HOOKS_ENDPOINT"], +) + +agent = Agent( + client=OpenAIChatClient(), + name="governed", + middleware=[bundle], # bundle, not a flat list +) +``` + +--- + +## 4. `ConversationSplit` and `ConversationSplitter` + +**Module:** `agent_framework._evaluation` (re-exported via `agent_framework`) + +> **Experimental:** requires `ExperimentalFeature.EVALS` to be acknowledged. + +These two types work together in the evaluation harness. `ConversationSplitter` is a **structural protocol** — any callable with the signature `(list[Message]) → tuple[list[Message], list[Message]]` satisfies it. `ConversationSplit` is an **enum** of built-in splitters that also satisfy the protocol. + +### `ConversationSplit` enum + +| Member | Value | Behaviour | +|---|---|---| +| `ConversationSplit.LAST_TURN` | `"last_turn"` | Query = everything up to and including the last user message; response = all messages after. Evaluates whether the agent answered the *latest* question well. | +| `ConversationSplit.FULL` | `"full"` | Query = the first user message (plus any preceding system messages); response = the whole remainder. Evaluates the *complete conversation trajectory*. | + +Both members are callable: `query_msgs, response_msgs = ConversationSplit.LAST_TURN(conversation)`. + +### `ConversationSplitter` protocol + +```python +# Any callable with this signature satisfies ConversationSplitter: +def my_splitter( + conversation: list[Message], +) -> tuple[list[Message], list[Message]]: + ... +``` + +### Example — built-in split with `LocalEvaluator` + +```python +import asyncio +from agent_framework import ( + Agent, EvalItem, LocalEvaluator, ConversationSplit, + acknowledge_experimental_feature, ExperimentalFeature, +) +from agent_framework.openai import OpenAIChatClient + +acknowledge_experimental_feature(ExperimentalFeature.EVALS) + + +async def main(): + judge = Agent( + client=OpenAIChatClient(), + name="judge", + instructions=( + "You are an impartial judge. Given a query and an agent response, " + "reply with PASS or FAIL followed by a one-sentence reason." + ), + ) + evaluator = LocalEvaluator(judge_agent=judge) + + item = EvalItem( + conversation=[ + {"role": "user", "content": "What is the capital of France?"}, + {"role": "assistant", "content": "The capital of France is Paris."}, + ] + ) + + result = await evaluator.evaluate( + items=[item], + split=ConversationSplit.LAST_TURN, + ) + for r in result.results: + print(r.passed, r.reason) + +asyncio.run(main()) +``` + +### Example — custom `ConversationSplitter` + +```python +from agent_framework import Message + + +def split_before_tool_call( + conversation: list[Message], +) -> tuple[list[Message], list[Message]]: + """Split just before the first tool call, to evaluate what led the agent to call the tool.""" + for i, msg in enumerate(conversation): + for content in (msg.contents or []): + if content.type == "function_call": + return conversation[:i], conversation[i:] + # Fallback: last-turn split + from agent_framework import ConversationSplit + return ConversationSplit.LAST_TURN(conversation) + + +# Use exactly like a built-in split: +# evaluator.evaluate(items=items, split=split_before_tool_call) +``` + +### Example — `FULL` split for trajectory evaluation + +```python +from agent_framework import ConversationSplit, Message + +conversation = [ + Message.from_user("Plan a weekend trip to London."), + Message.from_assistant("Sure! Day 1: Arrive and check into your hotel..."), + Message.from_user("What about museums?"), + Message.from_assistant("London has the British Museum, the Tate Modern, and the Natural History Museum..."), +] + +query, response = ConversationSplit.FULL(conversation) +print("Query messages:", [m.role for m in query]) # ['user'] +print("Response messages:", [m.role for m in response]) # ['assistant', 'user', 'assistant'] +``` + +--- + +## 5. `VectorStoreHistoryProvider` + +**Module:** `agent_framework._vectors` (re-exported via `agent_framework`) + +> **Experimental:** requires `ExperimentalFeature.VECTOR_STORES` to be acknowledged. + +`VectorStoreHistoryProvider` stores full conversation history in a provider-owned vector collection. Unlike `VectorCollectionContextProvider` (which exposes a caller-owned data model), this provider owns the collection schema and translates `Message` objects into a fixed history schema with optional embedding support. + +History is scoped by `application_id` + optional `tenant_id` + optional `agent_id` + `source_id` + session ID. This prevents overlap between different agents and tenants but is **not** an authorization boundary — use appropriately scoped store credentials. + +### Constructor + +```python +VectorStoreHistoryProvider( + vector_store: BaseVectorStore, + source_id: str = "vector_store_history", + *, + application_id: str, # required + tenant_id: str | None = None, + agent_id: str | None = None, + collection_name: str | None = None, # required when embedding_generator is set + contents_format: Literal["json", "msgpack"] = "json", + embedding_generator: EmbeddingClient | None = None, + embedding_options: Mapping[str, Any] | None = None, # must include "dimensions" when embedding_generator is set + compaction_strategy: CompactionStrategy | None = None, + compaction_tokenizer: TokenizerProtocol | None = None, + include_search_tool: bool = False, # requires embedding_generator + search_approval_mode: Literal["always_require", "never_require"] = "never_require", + load_messages: bool = True, + store_inputs: bool = True, + store_context_messages: bool = False, + store_context_from: set[str] | None = None, + store_outputs: bool = True, +) +``` + +| Parameter | Notes | +|---|---| +| `application_id` | Required. Isolates this application's history from all others. | +| `collection_name` | Required when `embedding_generator` is supplied; otherwise derived automatically. | +| `embedding_options` | Must include `"dimensions": int` when `embedding_generator` is supplied. | +| `include_search_tool` | Adds a scoped `search_history` tool to the agent; requires embedding. | +| `contents_format` | `"json"` (text) or `"msgpack"` (base64-encoded binary). | +| `store_context_messages` | Whether to also persist context injected by other providers. | +| `store_context_from` | Restrict context persistence to specific source IDs. | + +### Constants + +| Constant | Value | +|---|---| +| `DEFAULT_SOURCE_ID` | `"vector_store_history"` | +| `SEARCH_TOOL_NAME` | `"search_history"` | +| `SEARCH_TOOL_DESCRIPTION` | `"Search the full conversation history..."` | + +### Methods + +| Method | Signature | Notes | +|---|---|---| +| `get_messages` | `async (session_id, *, state, **kwargs) → list[Message]` | Returns the full scoped transcript, sorted by creation time. | +| `save_messages` | `async (session_id, messages, *, state, **kwargs) → None` | Upserts new messages; assigns IDs to messages that lack one. | +| `clear` | `async (session_id) → None` | Deletes all records for the scoped history. | +| `before_run` | `async (*, agent, session, context, state) → None` | Loads history, runs optional compaction, adds search tool. | + +### Example — basic vector-backed history + +```python +import asyncio +from agent_framework import ( + Agent, acknowledge_experimental_feature, ExperimentalFeature, +) +from agent_framework._vectors import VectorStoreHistoryProvider +from agent_framework.openai import OpenAIChatClient + +# Use any supported vector store, e.g. InMemoryStore (already deep-dived in Vol. 4) +from agent_framework import InMemoryStore + +acknowledge_experimental_feature(ExperimentalFeature.VECTOR_STORES) + + +async def main(): + store = InMemoryStore() + history_provider = VectorStoreHistoryProvider( + store, + application_id="my-app", + agent_id="support-bot", + ) + + agent = Agent( + client=OpenAIChatClient(), + name="support-bot", + instructions="You are a helpful customer-support agent.", + context_providers=[history_provider], + ) + + session = agent.create_session() + await agent.run("Hi, I need help with my order.", session=session) + await agent.run("It's order #12345.", session=session) + + # Retrieve persisted history directly + msgs = await history_provider.get_messages(session.session_id) + print(f"Stored {len(msgs)} messages.") + +asyncio.run(main()) +``` + +### Example — history with semantic search tool + +```python +import asyncio +from agent_framework import ( + Agent, InMemoryStore, acknowledge_experimental_feature, ExperimentalFeature, +) +from agent_framework._vectors import VectorStoreHistoryProvider +from agent_framework.openai import OpenAIChatClient, OpenAIEmbeddingClient + +acknowledge_experimental_feature(ExperimentalFeature.VECTOR_STORES) + + +async def main(): + store = InMemoryStore() + history_provider = VectorStoreHistoryProvider( + store, + application_id="semantic-app", + collection_name="chat-history-v1", + embedding_generator=OpenAIEmbeddingClient(model="text-embedding-3-small"), + embedding_options={"dimensions": 1536}, + include_search_tool=True, # adds search_history tool to the agent + search_approval_mode="never_require", + ) + + agent = Agent( + client=OpenAIChatClient(), + instructions="Use search_history to recall past conversations.", + context_providers=[history_provider], + ) + + session = agent.create_session() + await agent.run("My favourite colour is blue.", session=session) + # In a later session the agent can search for "favourite colour" + result = await agent.run("What did I say about colours?", session=session) + print(result.text) + +asyncio.run(main()) +``` + +### Example — clear session history + +```python +async def reset_user_history(history_provider, session_id: str): + await history_provider.clear(session_id) + print(f"Cleared history for session {session_id!r}.") +``` + +--- + +## 6. `MemoryStore` (ABC) and `MemoryFileStore` + +**Module:** `agent_framework._harness._memory` (re-exported via `agent_framework`) + +> **Experimental:** requires `ExperimentalFeature.HARNESS` to be acknowledged. + +`MemoryStore` is the **abstract base class** for all memory backing stores used by `FileMemoryProvider`. It manages topic-based long-term memory organised as a set of per-topic markdown files plus a `MEMORY.md` index and a transcript archive. + +`MemoryFileStore` is the concrete filesystem implementation provided by the framework. + +### `MemoryStore` abstract interface + +| Method | Signature | Notes | +|---|---|---| +| `get_owner_id` | `(session) → str \| None` | Logical owner for isolation. Default returns `None`. | +| `export_provider_state` | `(session) → dict[str, Any]` | Routing metadata needed to reopen storage across sessions. | +| `import_provider_state` | `(session, *, state) → None` | Restore routing metadata onto a temporary session. | +| `list_topics` | `(session, *, source_id) → list[MemoryTopicRecord]` | **Abstract.** All topic files for the current owner. | +| `get_topic` | `(session, *, source_id, topic) → MemoryTopicRecord` | **Abstract.** One topic by name or slug. | +| `write_topic` | `(session, record, *, source_id) → None` | **Abstract.** Persist a topic file. | +| `delete_topic` | `(session, *, source_id, topic) → None` | **Abstract.** Remove a topic file. | +| `rebuild_index` | `(session, *, source_id, line_limit, line_length) → list[MemoryIndexEntry]` | **Abstract.** Rebuild `MEMORY.md` from current topic files. | +| `get_index_text` | `(session, *, source_id, line_limit, line_length, index_entries=None) → str` | **Abstract.** Return current `MEMORY.md` text. | +| `read_state` | `(session, *, source_id) → dict[str, Any]` | **Abstract.** Read maintenance state JSON. | +| `write_state` | `(session, state, *, source_id) → None` | **Abstract.** Write maintenance state JSON. | +| `get_transcripts_directory` | `(session, *, source_id) → Path` | **Abstract.** Owner-level transcript archive directory. | +| `search_transcripts` | `(session, *, source_id, query, session_id=None, limit=20) → list[dict]` | **Abstract.** Full-text search over the JSONL transcript archive. | + +### `MemoryFileStore` constructor + +```python +MemoryFileStore( + base_path: str | Path, + *, + kind: str = "memory", + owner_prefix: str = "", + owner_state_key: str, # session state key holding the logical owner ID + index_file_name: str = "MEMORY.md", + topics_directory_name: str = "topics", + transcripts_directory_name: str = "transcripts", + state_file_name: str = "state.json", + dumps: JsonDumps | None = None, + loads: JsonLoads | None = None, +) +``` + +| Parameter | Notes | +|---|---| +| `base_path` | Root directory for all memory data. | +| `owner_state_key` | Session state key that resolves to the logical owner ID (e.g. user ID). Required. | +| `kind` | Subdirectory bucket name within each owner root. Useful to separate memory types. | +| `owner_prefix` | String prepended to the resolved owner ID for namespacing. | +| `dumps` / `loads` | Custom JSON serialization hooks (defaults to `json.dumps` / `json.loads`). | + +**Path resolution** follows `base_path / source_component / owner_component / kind`. Path traversal in owner IDs (`..", absolute paths) raises `ValueError`. + +### Example — file-backed memory with `FileMemoryProvider` + +```python +import asyncio +from agent_framework import ( + Agent, FileMemoryProvider, acknowledge_experimental_feature, ExperimentalFeature, +) +from agent_framework._harness._memory import MemoryFileStore +from agent_framework.openai import OpenAIChatClient + +acknowledge_experimental_feature(ExperimentalFeature.HARNESS) + + +async def main(): + store = MemoryFileStore( + base_path="/tmp/agent-memory", + owner_state_key="user_id", + ) + + provider = FileMemoryProvider( + memory_store=store, + memory_agent=Agent( + client=OpenAIChatClient(), + name="memory-agent", + instructions="Consolidate and maintain the user's long-term memories.", + ), + ) + + agent = Agent( + client=OpenAIChatClient(), + name="assistant", + instructions="You remember details about the user from past sessions.", + context_providers=[provider], + ) + + session = agent.create_session() + session.state["user_id"] = "user-42" + + result = await agent.run("My cat's name is Mochi.", session=session) + print(result.text) + + result2 = await agent.run("What's my cat's name?", session=session) + print(result2.text) # Should recall "Mochi" + +asyncio.run(main()) +``` + +### Example — custom `MemoryStore` implementation + +```python +from pathlib import Path +from agent_framework import AgentSession +from agent_framework._harness._memory import MemoryStore, MemoryTopicRecord, MemoryIndexEntry + + +class InMemoryMemoryStore(MemoryStore): + """In-memory MemoryStore for unit testing.""" + + def __init__(self): + self._topics: dict[str, dict[str, MemoryTopicRecord]] = {} # owner → slug → record + self._states: dict[str, dict] = {} + self._tmp = Path("/tmp/in-memory-store-transcripts") + + def _owner(self, session: AgentSession) -> str: + return str(session.state.get("user_id", "default")) + + def list_topics(self, session, *, source_id): + return sorted(self._topics.get(self._owner(session), {}).values(), + key=lambda r: r.topic) + + def get_topic(self, session, *, source_id, topic): + record = self._topics.get(self._owner(session), {}).get(topic) + if record is None: + raise FileNotFoundError(topic) + return record + + def write_topic(self, session, record, *, source_id): + self._topics.setdefault(self._owner(session), {})[record.slug] = record + + def delete_topic(self, session, *, source_id, topic): + self._topics.get(self._owner(session), {}).pop(topic, None) + + def rebuild_index(self, session, *, source_id, line_limit, line_length): + return [MemoryIndexEntry.from_topic_record(t) for t in self.list_topics(session, source_id=source_id)] + + def get_index_text(self, session, *, source_id, line_limit, line_length, index_entries=None): + entries = index_entries or self.rebuild_index(session, source_id=source_id, + line_limit=line_limit, line_length=line_length) + return "\n".join(e.to_pointer_line(max_length=line_length) for e in entries) + + def read_state(self, session, *, source_id): + return dict(self._states.get(self._owner(session), {})) + + def write_state(self, session, state, *, source_id): + self._states[self._owner(session)] = dict(state) + + def get_transcripts_directory(self, session, *, source_id): + self._tmp.mkdir(parents=True, exist_ok=True) + return self._tmp + + def search_transcripts(self, session, *, source_id, query, session_id=None, limit=20): + return [] +``` + +--- + +## 7. `MemoryTopicRecord` + +**Module:** `agent_framework._harness._memory` (re-exported via `agent_framework`) + +> **Experimental:** requires `ExperimentalFeature.HARNESS`. + +`MemoryTopicRecord` represents one **topic memory file** — the unit of long-term memory storage. Each record has a human-readable topic, a stable `slug` (filesystem name), a short `summary`, a deduplicated list of `memories` (bullet points), a timestamp, and the session IDs that contributed to this topic. + +### Constructor + +```python +MemoryTopicRecord( + *, + topic: str, + slug: str | None = None, # derived from topic if omitted + summary: str, + memories: Sequence[str], + updated_at: str, # ISO 8601 timestamp string + session_ids: Sequence[str] | None = None, +) +``` + +| Parameter | Notes | +|---|---| +| `topic` | Human-readable name. Normalised (whitespace collapsed, stripped). | +| `slug` | Filesystem stem for the `.md` file. Derived automatically from `topic` when omitted. | +| `memories` | Deduplicated list of durable bullet-point memories. | +| `summary` | Short topic summary. Required for meaningful index rendering. | +| `updated_at` | Last-updated ISO timestamp. | +| `session_ids` | Sessions that contributed to this topic. Deduplicated. | + +### Attributes (all from `__slots__`) + +`topic`, `slug`, `summary`, `memories`, `updated_at`, `session_ids`. + +### Methods + +| Method | Notes | +|---|---| +| `to_dict() → dict[str, Any]` | JSON-compatible serialization. | +| `from_dict(raw_record) → MemoryTopicRecord` | Deserialize from a dict. Validates required fields. | +| `to_markdown() → str` | Render the canonical on-disk markdown format. | +| `from_markdown(markdown, *, fallback_topic=None) → MemoryTopicRecord` | Parse from the canonical markdown format. | +| `__eq__` | Value equality via `to_dict()`. | + +### Markdown format + +```markdown +# Travel Plans + +Updated: 2025-10-01T10:00:00 +Sessions: session-001, session-002 + +## Summary +User's upcoming travel plans and preferences. + +## Memories +- User is flying to Tokyo in November 2025. +- User prefers window seats on long-haul flights. +- User wants to visit Shibuya and Shinjuku. +``` + +### Example — creating and serializing a topic record + +```python +from agent_framework._harness._memory import MemoryTopicRecord + +record = MemoryTopicRecord( + topic="Travel Plans", + summary="User's upcoming travel plans and preferences.", + memories=[ + "User is flying to Tokyo in November 2025.", + "User prefers window seats on long-haul flights.", + "User wants to visit Shibuya and Shinjuku.", + ], + updated_at="2025-10-01T10:00:00", + session_ids=["session-001"], +) + +print(record.slug) # "travel-plans" +print(record.to_markdown()) +print(record.to_dict()) +``` + +### Example — round-tripping through markdown + +```python +from agent_framework._harness._memory import MemoryTopicRecord + +original = MemoryTopicRecord( + topic="Dietary Preferences", + summary="User's food preferences and restrictions.", + memories=["User is lactose intolerant.", "User loves sushi."], + updated_at="2025-09-15T08:30:00", +) + +md = original.to_markdown() +restored = MemoryTopicRecord.from_markdown(md) +assert restored == original +``` + +### Example — searching memory topics + +```python +from agent_framework._harness._memory import MemoryFileStore, MemoryTopicRecord +from agent_framework import AgentSession + + +def find_topics_matching(store: MemoryFileStore, session: AgentSession, keyword: str): + """Return all topics whose memories contain the given keyword.""" + return [ + record + for record in store.list_topics(session, source_id="memory") + if any(keyword.lower() in m.lower() for m in record.memories) + ] +``` + +--- + +## 8. `WorkflowRunResult` + +**Module:** `agent_framework._workflows._workflow` (re-exported via `agent_framework`) + +`WorkflowRunResult` is a `list[WorkflowEvent]` subclass returned by `await workflow.run(...)`. It holds the **data-plane** events (executor invocations, completions, outputs, and `request_info` pauses) in the list itself, and the **control-plane** status events in a separate private list accessible via `status_timeline()`. + +### Constructor + +```python +WorkflowRunResult( + events: list[WorkflowEvent[Any]], + status_events: list[WorkflowEvent[Any]] | None = None, +) +``` + +> The framework constructs `WorkflowRunResult` for you. You receive it from `await workflow.run(...)`. + +### Methods + +| Method | Returns | Notes | +|---|---|---| +| `get_outputs()` | `list[Any]` | Data from every `"output"` event. The typical way to extract final results. | +| `get_intermediate_outputs()` | `list[Any]` | Data from every `"intermediate"` event. | +| `get_request_info_events()` | `list[WorkflowEvent[Any]]` | Pause events requesting external input. Non-empty when final state is `IDLE_WITH_PENDING_REQUESTS`. | +| `get_final_state()` | `WorkflowRunState` | Last status event's state. Raises `RuntimeError` if no status events were emitted. | +| `status_timeline()` | `list[WorkflowEvent[Any]]` | Ordered list of all status-transition events (control-plane copy). | + +Because `WorkflowRunResult` subclasses `list`, you can iterate over it directly to process all data-plane events. + +### `WorkflowRunState` enum + +| Value | Meaning | +|---|---| +| `STARTED` | Run has begun. | +| `IN_PROGRESS` | Executors are running. | +| `IN_PROGRESS_PENDING_REQUESTS` | Running but paused waiting for external input. | +| `IDLE` | Run completed cleanly. | +| `IDLE_WITH_PENDING_REQUESTS` | Run paused; `get_request_info_events()` has pending items. | +| `FAILED` | Run terminated with an error. | +| `CANCELLED` | Run was cancelled. | + +### Example — basic result inspection + +```python +import asyncio +from agent_framework import Agent, WorkflowBuilder, WorkflowRunState +from agent_framework.openai import OpenAIChatClient + + +async def main(): + agent = Agent( + client=OpenAIChatClient(), + name="haiku-writer", + instructions="Write a haiku about the given subject.", + ) + workflow = WorkflowBuilder().add_agent(agent).build() + result = await workflow.run("spring rain") + + # Primary output + outputs = result.get_outputs() + print(f"Outputs: {outputs}") + + # Final state + state = result.get_final_state() + print(f"Final state: {state.value}") + assert state == WorkflowRunState.IDLE + + # Status timeline + for ev in result.status_timeline(): + print(f" {ev.state.value}") + +asyncio.run(main()) +``` + +### Example — multi-agent pipeline output extraction + +```python +import asyncio +from agent_framework import Agent, WorkflowBuilder +from agent_framework.openai import OpenAIChatClient + + +async def main(): + client = OpenAIChatClient() + researcher = Agent(client=client, name="researcher", + instructions="Research the given topic and write 3 key facts.") + writer = Agent(client=client, name="writer", + instructions="Turn the researcher's facts into a polished paragraph.") + + workflow = ( + WorkflowBuilder() + .add_agent(researcher) + .add_agent(writer, input_from=["researcher"]) + .build() + ) + result = await workflow.run("quantum computing") + + # All outputs in order + for i, output in enumerate(result.get_outputs()): + print(f"Output {i + 1}: {output}") + + # Only the writer's output + writer_outputs = [ + ev.data for ev in result + if ev.type == "output" and ev.executor_id == "writer" + ] + print(f"Writer: {writer_outputs}") + +asyncio.run(main()) +``` + +### Example — detecting pending requests + +```python +import asyncio +from agent_framework import WorkflowRunState + + +async def run_and_handle(workflow, initial_prompt: str): + result = await workflow.run(initial_prompt) + + if result.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS: + for req_event in result.get_request_info_events(): + print(f"Workflow is asking ({req_event.source_executor_id}): {req_event.data}") + answer = input("Your answer: ") + result = await workflow.respond( + request_id=req_event.request_id, + response=answer, + ) + + return result.get_outputs() +``` + +--- + +## 9. `FunctionInvocationContext` + +**Module:** `agent_framework._middleware` (re-exported via `agent_framework`) + +`FunctionInvocationContext` is the mutable context object passed through the **function middleware** pipeline on every tool invocation. It mirrors `AgentContext` but scopes to a single tool call. A key capability added in 1.16.0+ is **progressive tool exposure**: tools can add or remove other tools from the live run via `add_tools()` / `remove_tools()`. + +### Constructor + +```python +FunctionInvocationContext( + function: FunctionTool, + arguments: BaseModel | Mapping[str, Any], + session: AgentSession | None = None, + metadata: Mapping[str, Any] | None = None, + result: Any = None, + kwargs: Mapping[str, Any] | None = None, + tools: list[ToolTypes] | None = None, # live tool list for progressive exposure +) +``` + +> The framework constructs `FunctionInvocationContext` for you. + +### Attributes + +| Attribute | Type | Notes | +|---|---|---| +| `function` | `FunctionTool` | The tool being called. | +| `arguments` | `BaseModel \| Mapping[str, Any]` | Parsed tool arguments. May be raw JSON-parsed mapping if provisional validation failed. | +| `session` | `AgentSession \| None` | Current session, or `None` outside a session run. | +| `metadata` | `dict[str, Any]` | Scratchpad shared between function middleware layers on this invocation. | +| `result` | `Any` | Tool result set after `call_next()`. Replace to override. `list[Content]` or `str` passed through intact; other types are stringified. | +| `kwargs` | `dict[str, Any]` | Extra kwargs forwarded to the tool. | +| `tools` | `list[ToolTypes] \| None` | **Live** mutable tool list for the current agent run. `None` outside a function-calling loop. | + +### Methods (experimental: `ExperimentalFeature.PROGRESSIVE_TOOLS`) + +```python +context.add_tools( + tools: ToolTypes | Callable | Sequence[...] +) → None +``` +Add tools to the live run. Duplicate names raise `ValueError` if the duplicate is a different object. Takes effect on the **next** model iteration. + +```python +context.remove_tools( + tools: ToolTypes | Callable | Sequence[...] | str | Sequence[str] +) → None +``` +Remove tools by object, callable, or name string. Unknown names are silently ignored. Takes effect on the next model iteration. + +### Example — argument logging middleware + +```python +import json +from agent_framework import FunctionMiddleware, FunctionInvocationContext + + +class ToolAuditMiddleware(FunctionMiddleware): + async def process(self, context: FunctionInvocationContext, call_next): + args = context.arguments + args_str = json.dumps(dict(args), default=str) if hasattr(args, "items") else repr(args) + print(f"[Audit] {context.function.name}({args_str})") + + await call_next() + + print(f"[Audit] {context.function.name} → {context.result!r}") +``` + +### Example — argument sanitisation middleware + +```python +from agent_framework import FunctionMiddleware, FunctionInvocationContext, MiddlewareTermination + + +class SqlInjectionGuard(FunctionMiddleware): + BLOCKED = {"'; drop table", "union select", "--"} + + async def process(self, context: FunctionInvocationContext, call_next): + for value in (context.arguments or {}).values(): + if isinstance(value, str): + lower = value.lower() + if any(bad in lower for bad in self.BLOCKED): + raise MiddlewareTermination("Blocked: potential SQL injection.") + await call_next() +``` + +### Example — progressive tool exposure + +```python +import asyncio +from agent_framework import ( + Agent, FunctionInvocationContext, tool, + acknowledge_experimental_feature, ExperimentalFeature, +) +from agent_framework.openai import OpenAIChatClient + +acknowledge_experimental_feature(ExperimentalFeature.PROGRESSIVE_TOOLS) + + +@tool +def factorial(n: int) -> int: + """Compute n!""" + result = 1 + for i in range(2, n + 1): + result *= i + return result + + +@tool +def fibonacci(n: int) -> int: + """Return the n-th Fibonacci number.""" + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a + + +@tool +def load_math_tools(ctx: FunctionInvocationContext) -> str: + """Load advanced math tools into this conversation.""" + ctx.add_tools([factorial, fibonacci]) + return "Math tools loaded: factorial, fibonacci." + + +async def main(): + agent = Agent( + client=OpenAIChatClient(), + name="math-bot", + instructions="You have access to load_math_tools. Call it before doing factorial or fibonacci calculations.", + tools=[load_math_tools], # only this tool initially + ) + result = await agent.run("What is 7! and the 10th Fibonacci number?") + print(result.text) + +asyncio.run(main()) +``` + +### Example — injecting per-call kwargs via agent middleware + +```python +from agent_framework import AgentMiddleware, AgentContext + + +class TraceIdMiddleware(AgentMiddleware): + """Inject a trace ID into every tool invocation.""" + + def __init__(self, trace_id: str): + self._trace_id = trace_id + + async def process(self, context: AgentContext, call_next): + context.function_invocation_kwargs["trace_id"] = self._trace_id + await call_next() +``` + +--- + +## 10. `ChatOptions` + +**Module:** `agent_framework._types` (re-exported via `agent_framework`) + +`ChatOptions` is a `TypedDict` (total=False) that describes the common request parameters accepted by all `agent_framework` chat clients. All fields are **optional**, allowing partial specification. Individual providers may raise errors for unsupported options. + +### Fields + +| Field | Type | Notes | +|---|---|---| +| `model` | `str` | Model identifier (e.g. `"gpt-4o"`, `"claude-sonnet-5"`). | +| `temperature` | `float` | Sampling temperature (0–2 for most providers). | +| `top_p` | `float` | Nucleus sampling probability mass. | +| `max_tokens` | `int` | Maximum tokens in the response. | +| `stop` | `str \| Sequence[str]` | Stop sequence(s). | +| `seed` | `int` | Seed for deterministic sampling (provider-dependent). | +| `logit_bias` | `dict[str \| int, float]` | Token-level probability adjustments. | +| `frequency_penalty` | `float` | Penalise token frequency. | +| `presence_penalty` | `float` | Penalise token presence. | +| `tools` | tool types | Tool set for this call. | +| `tool_choice` | `ToolMode \| "auto" \| "required" \| "none"` | How the model selects tools. | +| `allow_multiple_tool_calls` | `bool` | Whether to allow more than one tool call per response. | +| `response_format` | `type[BaseModel] \| Mapping[str, Any] \| None` | Structured output schema. | +| `metadata` | `dict[str, Any]` | Provider-specific metadata. | +| `user` | `str` | End-user identifier (e.g. for OpenAI abuse monitoring). | +| `store` | `bool` | Whether to persist the request for fine-tuning (OpenAI). | +| `conversation_id` | `str` | Conversation identifier (provider-specific). | +| `instructions` | `str` | System-level instructions override. | + +### Usage patterns + +`ChatOptions` is used as: + +1. **`Agent` constructor `default_options`** — applied on every run unless overridden. +2. **Per-run override** via `agent.run(..., **options)`. +3. **`Unpack[ChatOptions]` function signatures** for type-safe option forwarding. + +### Example — setting default options on an agent + +```python +import asyncio +from agent_framework import Agent, ChatOptions +from agent_framework.openai import OpenAIChatClient + + +async def main(): + options: ChatOptions = { + "model": "gpt-4o-mini", + "temperature": 0.3, + "max_tokens": 512, + } + + agent = Agent( + client=OpenAIChatClient(), + name="concise-bot", + instructions="Be brief.", + default_options=options, + ) + + result = await agent.run("Explain black holes.") + print(result.text) + +asyncio.run(main()) +``` + +### Example — per-run override + +```python +import asyncio +from agent_framework import Agent +from agent_framework.openai import OpenAIChatClient + + +async def main(): + agent = Agent( + client=OpenAIChatClient(), + instructions="You are a creative writer.", + default_options={"temperature": 0.7}, + ) + + # Override temperature for this specific call + creative_result = await agent.run("Write a haiku.", temperature=1.2) + print(creative_result.text) + + # Use a different model for a specific call + fast_result = await agent.run("Summarize AI.", model="gpt-4o-mini", max_tokens=50) + print(fast_result.text) + +asyncio.run(main()) +``` + +### Example — structured output with `response_format` + +```python +import asyncio +from pydantic import BaseModel +from agent_framework import Agent, ChatOptions +from agent_framework.openai import OpenAIChatClient + + +class Haiku(BaseModel): + line1: str + line2: str + line3: str + + +async def main(): + options: ChatOptions = { + "response_format": Haiku, + "temperature": 0.9, + } + + agent = Agent( + client=OpenAIChatClient(), + instructions="Write a haiku about the given subject. Reply as JSON.", + default_options=options, + ) + + result = await agent.run("autumn leaves") + haiku = Haiku.model_validate_json(result.text) + print(f"{haiku.line1} / {haiku.line2} / {haiku.line3}") + +asyncio.run(main()) +``` + +### Example — type-safe option forwarding + +```python +from typing import Unpack +from agent_framework import Agent, ChatOptions +from agent_framework.openai import OpenAIChatClient + + +class ConfigurableAgent: + def __init__(self, **default_options: Unpack[ChatOptions]): + self._agent = Agent( + client=OpenAIChatClient(), + instructions="You are a helpful assistant.", + default_options=dict(default_options), + ) + + async def ask(self, prompt: str, **override: Unpack[ChatOptions]) -> str: + result = await self._agent.run(prompt, **override) + return result.text + + +# Usage: +# bot = ConfigurableAgent(model="gpt-4o", temperature=0.5) +# answer = await bot.ask("What is ML?", max_tokens=100) +``` + +--- + +## What's new in 1.19.0 + +The 1.19.0 release refines several of the APIs deep-dived in this and prior volumes. Key areas: + +| Area | Change | +|---|---| +| **Progressive tools** | `FunctionInvocationContext.add_tools()` / `remove_tools()` stabilised under `ExperimentalFeature.PROGRESSIVE_TOOLS`. All-or-nothing batch semantics: a duplicate name raises before the live list is mutated. | +| **Vector history** | `VectorStoreHistoryProvider` adds `store_context_from` for fine-grained control over which source IDs have their context messages persisted. | +| **Memory harness** | `MemoryFileStore.search_transcripts` now resolves the target transcript file stem via `_transcript_file_stem()` — supporting even very long session IDs stored under an irreversible digest. | +| **WorkflowEvent** | `WorkflowEvent.executor_bypassed` documents the cache-hit replay path more precisely. The `emit()` factory deprecation warning is now emitted with `stacklevel=2` for correct source attribution. | +| **ChatOptions** | `conversation_id` field added for providers that support conversation-level threading. | + +--- + +## See also + +- [Python Comprehensive Guide](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/) — framework overview with 1.18.0 feature table +- [Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/) — workflow visualization, file memory, background agents, tool approval +- [Class Deep Dives Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/) — fan-in/out edges, functional workflows, checkpointing, MCP tools +- [Class Deep Dives Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/) — workflow builder, compaction strategies, evaluation, inline skills, file access +- [Class Deep Dives Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/) — vector store fields, in-memory collections, filters, settings, group chat diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md index b4c0869e..65b65868 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md @@ -1,20 +1,20 @@ --- title: "Microsoft Agent Framework Python - Comprehensive Technical Guide" -description: "Comprehensive technical guide for the Microsoft Agent Framework on Python. Guide content verified against agent-framework 1.14.0 (latest PyPI release: 1.18.0) — chat clients, tools, sessions, middleware, MCP, skills, workflows, long-term memory, evaluation, security (FIDES), file access/memory providers, workflow visualization, and observability. For class examples verified against 1.15.0–1.18.0, see the Class Deep Dives Vol. 1–4 guides." +description: "Comprehensive technical guide for the Microsoft Agent Framework on Python. Guide content verified against agent-framework 1.14.0 (latest PyPI release: 1.19.0) — chat clients, tools, sessions, middleware, MCP, skills, workflows, long-term memory, evaluation, security (FIDES), file access/memory providers, workflow visualization, and observability. For class examples verified against 1.15.0–1.19.0, see the Class Deep Dives Vol. 1–5 guides." framework: microsoft-agent-framework language: python --- -Latest: agent-framework 1.18.0 | Guide verified against: 1.14.0 | Python 3.10+ +Latest: agent-framework 1.19.0 | Guide verified against: 1.14.0 | Python 3.10+ # Microsoft Agent Framework Python - Comprehensive Technical Guide -**Framework Version (guide content):** 1.14.0 (`agent-framework` and `agent-framework-core`) | **Latest PyPI release:** 1.18.0 +**Framework Version (guide content):** 1.14.0 (`agent-framework` and `agent-framework-core`) | **Latest PyPI release:** 1.19.0 **Target Platform:** Python 3.10+ **Quick check:** `pip index versions agent-framework` --- -> **API reference (verified against `agent-framework==1.14.0`; 40 APIs (38 classes and 2 functions) verified on 1.15.0–1.18.0 — see [10-Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/), [Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/), [Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/) and [Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/)).** +> **API reference (verified against `agent-framework==1.14.0`; 50 APIs (48 classes and 2 functions) verified on 1.15.0–1.19.0 — see [10-Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/), [Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/), [Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/), [Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/) and [Vol. 5](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5/)).** > > - **Package name / import root:** `agent_framework` (underscores). Install with `pip install agent-framework`. > - **Agent classes:** `Agent` (full stack with middleware + telemetry), `RawAgent` (same interface, skips the middleware/telemetry wrappers for latency-sensitive paths), `BaseAgent` (abstract base for custom subclasses). From daab4d0e0377b83efac856c187fa0a2dc8bef90f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 09:51:09 +0000 Subject: [PATCH 02/22] fix(vol5): correct 8 API bugs found in Codex review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All bugs were in code examples in the Vol. 5 class deep dives doc: - WorkflowBuilder: use start_executor= kwarg; replace .add_agent()/.add_agent() chain with .add_edge() since add_agent() does not exist - Streaming: use workflow.run(..., stream=True) instead of workflow.stream() - HITL resume: use workflow.run(responses={id: answer}, checkpoint_id=...) instead of the non-existent workflow.respond() - LocalEvaluator: pass EvalCheck callables positionally; access .items not .results on EvalResults - create_agent_hooks_middleware: fix import to agent_framework (not _harness._hooks) and use interceptors= arg, not hooks_endpoint= - MemoryFileStore: pair with MemoryContextProvider(store=...) not FileMemoryProvider(memory_store=...) — MemoryFileStore backs MemoryContextProvider, not FileMemoryProvider - FunctionInvocationContext.arguments: handle BaseModel via model_dump() before iterating values - ChatOptions per-run: pass options={...} mapping, not bare **kwargs Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 96 +++++++++++-------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 87d9954a..8752b664 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -112,7 +112,7 @@ async def main(): agent = Agent(client=client, name="summarizer", instructions="Summarize the user's text in one sentence.") - workflow = WorkflowBuilder().add_agent(agent).build() + workflow = WorkflowBuilder(start_executor=agent).build() result = await workflow.run("The quick brown fox jumps over the lazy dog.") for event in result: @@ -139,9 +139,9 @@ from agent_framework.openai import OpenAIChatClient async def stream(): agent = Agent(client=OpenAIChatClient(), name="poet", instructions="Write a haiku.") - workflow = WorkflowBuilder().add_agent(agent).build() + workflow = WorkflowBuilder(start_executor=agent).build() - async for event in workflow.stream("Cherry blossoms fall"): + async for event in workflow.run("Cherry blossoms fall", stream=True): match event.type: case "started": print("Workflow started") @@ -166,8 +166,8 @@ import asyncio from agent_framework import WorkflowEvent # Pause-and-resume pattern: the executor emits a request_info event, -# the host reads it, supplies the answer, then resumes the workflow. -async def handle_pending_request(run_result, workflow): +# the host reads it, supplies the answer, then resumes the workflow via run(). +async def handle_pending_request(run_result, workflow, checkpoint_id: str): pending = run_result.get_request_info_events() if not pending: return @@ -177,9 +177,9 @@ async def handle_pending_request(run_result, workflow): print(f"Workflow is asking: {req.data}") user_answer = input("Your answer: ") - resumed = await workflow.respond( - request_id=req.request_id, - response=user_answer, + resumed = await workflow.run( + responses={req.request_id: user_answer}, + checkpoint_id=checkpoint_id, ) outputs = resumed.get_outputs() print(f"Final output: {outputs}") @@ -388,15 +388,21 @@ agent = Agent( ### Example — bundle returned by a factory (agent-hooks pattern) ```python -import os from agent_framework import Agent, acknowledge_experimental_feature, ExperimentalFeature -from agent_framework._harness._hooks import create_agent_hooks_middleware +from agent_framework import create_agent_hooks_middleware from agent_framework.openai import OpenAIChatClient acknowledge_experimental_feature(ExperimentalFeature.AGENT_HOOKS) + +# An interceptor is any callable matching the Interceptor protocol. +def my_interceptor(event_type: str, payload: dict) -> dict: + print(f"[Hook] event={event_type!r}") + return payload + + bundle = create_agent_hooks_middleware( - hooks_endpoint=os.environ["AGENT_HOOKS_ENDPOINT"], + interceptors=[my_interceptor], ) agent = Agent( @@ -440,7 +446,7 @@ def my_splitter( ```python import asyncio from agent_framework import ( - Agent, EvalItem, LocalEvaluator, ConversationSplit, + Agent, EvalItem, EvalCheck, CheckResult, LocalEvaluator, ConversationSplit, acknowledge_experimental_feature, ExperimentalFeature, ) from agent_framework.openai import OpenAIChatClient @@ -448,17 +454,18 @@ from agent_framework.openai import OpenAIChatClient acknowledge_experimental_feature(ExperimentalFeature.EVALS) -async def main(): - judge = Agent( - client=OpenAIChatClient(), - name="judge", - instructions=( - "You are an impartial judge. Given a query and an agent response, " - "reply with PASS or FAIL followed by a one-sentence reason." - ), +# An EvalCheck is a callable: (EvalItem) -> CheckResult +async def factual_check(item: EvalItem) -> CheckResult: + """Pass if the response contains 'Paris'.""" + response_text = " ".join( + m.get("content", "") if isinstance(m, dict) else (m.text or "") + for m in item.response ) - evaluator = LocalEvaluator(judge_agent=judge) + passed = "paris" in response_text.lower() + return CheckResult(passed=passed, reason="Response mentions Paris" if passed else "Missing 'Paris'") + +async def main(): item = EvalItem( conversation=[ {"role": "user", "content": "What is the capital of France?"}, @@ -466,11 +473,13 @@ async def main(): ] ) - result = await evaluator.evaluate( + evaluator = LocalEvaluator(factual_check) + + results = await evaluator.evaluate( items=[item], split=ConversationSplit.LAST_TURN, ) - for r in result.results: + for r in results.items: print(r.passed, r.reason) asyncio.run(main()) @@ -679,7 +688,7 @@ async def reset_user_history(history_provider, session_id: str): > **Experimental:** requires `ExperimentalFeature.HARNESS` to be acknowledged. -`MemoryStore` is the **abstract base class** for all memory backing stores used by `FileMemoryProvider`. It manages topic-based long-term memory organised as a set of per-topic markdown files plus a `MEMORY.md` index and a transcript archive. +`MemoryStore` is the **abstract base class** for all memory backing stores used by `MemoryContextProvider`. It manages topic-based long-term memory organised as a set of per-topic markdown files plus a `MEMORY.md` index and a transcript archive. `MemoryFileStore` is the concrete filesystem implementation provided by the framework. @@ -729,12 +738,12 @@ MemoryFileStore( **Path resolution** follows `base_path / source_component / owner_component / kind`. Path traversal in owner IDs (`..", absolute paths) raises `ValueError`. -### Example — file-backed memory with `FileMemoryProvider` +### Example — file-backed memory with `MemoryContextProvider` ```python import asyncio from agent_framework import ( - Agent, FileMemoryProvider, acknowledge_experimental_feature, ExperimentalFeature, + Agent, MemoryContextProvider, acknowledge_experimental_feature, ExperimentalFeature, ) from agent_framework._harness._memory import MemoryFileStore from agent_framework.openai import OpenAIChatClient @@ -748,8 +757,8 @@ async def main(): owner_state_key="user_id", ) - provider = FileMemoryProvider( - memory_store=store, + provider = MemoryContextProvider( + store=store, memory_agent=Agent( client=OpenAIChatClient(), name="memory-agent", @@ -1009,7 +1018,7 @@ async def main(): name="haiku-writer", instructions="Write a haiku about the given subject.", ) - workflow = WorkflowBuilder().add_agent(agent).build() + workflow = WorkflowBuilder(start_executor=agent).build() result = await workflow.run("spring rain") # Primary output @@ -1044,9 +1053,8 @@ async def main(): instructions="Turn the researcher's facts into a polished paragraph.") workflow = ( - WorkflowBuilder() - .add_agent(researcher) - .add_agent(writer, input_from=["researcher"]) + WorkflowBuilder(start_executor=researcher) + .add_edge(researcher, writer) .build() ) result = await workflow.run("quantum computing") @@ -1072,16 +1080,16 @@ import asyncio from agent_framework import WorkflowRunState -async def run_and_handle(workflow, initial_prompt: str): +async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): result = await workflow.run(initial_prompt) if result.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS: for req_event in result.get_request_info_events(): print(f"Workflow is asking ({req_event.source_executor_id}): {req_event.data}") answer = input("Your answer: ") - result = await workflow.respond( - request_id=req_event.request_id, - response=answer, + result = await workflow.run( + responses={req_event.request_id: answer}, + checkpoint_id=checkpoint_id, ) return result.get_outputs() @@ -1167,7 +1175,11 @@ class SqlInjectionGuard(FunctionMiddleware): BLOCKED = {"'; drop table", "union select", "--"} async def process(self, context: FunctionInvocationContext, call_next): - for value in (context.arguments or {}).values(): + args = context.arguments + args_dict = ( + args.model_dump() if hasattr(args, "model_dump") else dict(args or {}) + ) + for value in args_dict.values(): if isinstance(value, str): lower = value.lower() if any(bad in lower for bad in self.BLOCKED): @@ -1279,7 +1291,7 @@ class TraceIdMiddleware(AgentMiddleware): `ChatOptions` is used as: 1. **`Agent` constructor `default_options`** — applied on every run unless overridden. -2. **Per-run override** via `agent.run(..., **options)`. +2. **Per-run override** via `agent.run(..., options={...})`. 3. **`Unpack[ChatOptions]` function signatures** for type-safe option forwarding. ### Example — setting default options on an agent @@ -1325,12 +1337,12 @@ async def main(): default_options={"temperature": 0.7}, ) - # Override temperature for this specific call - creative_result = await agent.run("Write a haiku.", temperature=1.2) + # Override temperature for this specific call via options= + creative_result = await agent.run("Write a haiku.", options={"temperature": 1.2}) print(creative_result.text) # Use a different model for a specific call - fast_result = await agent.run("Summarize AI.", model="gpt-4o-mini", max_tokens=50) + fast_result = await agent.run("Summarize AI.", options={"model": "gpt-4o-mini", "max_tokens": 50}) print(fast_result.text) asyncio.run(main()) @@ -1387,7 +1399,7 @@ class ConfigurableAgent: ) async def ask(self, prompt: str, **override: Unpack[ChatOptions]) -> str: - result = await self._agent.run(prompt, **override) + result = await self._agent.run(prompt, options=dict(override)) return result.text From 5ec228193e1026c49e2a4c8fdf285b82f7d38111 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 09:54:13 +0000 Subject: [PATCH 03/22] fix(vol5): address second-round Copilot review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine additional corrections to code examples and prose: - include_status_events=True needed on workflow.run() for status/failed events to appear in the iterable list; without it they are only in status_timeline() - result.usage_details not result.usage (AgentResponse has no .usage) - AgentResponse(messages=...) not message= (wrong kwarg name) - LocalEvaluator.evaluate() has no split= param; split_strategy belongs on EvalItem(split_strategy=ConversationSplit.LAST_TURN) - VectorStoreHistoryProvider method table: show state=None default - Structured output: use result.value (validated model), not result.text + model_validate_json() - Fix mismatched quote in path-traversal note (``.."` → `..`) - typing.Unpack requires 3.11+; use typing_extensions for 3.10 compat - Update stale "See also" link version string to 1.19.0 Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 8752b664..a054fcfb 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -113,7 +113,12 @@ async def main(): instructions="Summarize the user's text in one sentence.") workflow = WorkflowBuilder(start_executor=agent).build() - result = await workflow.run("The quick brown fox jumps over the lazy dog.") + # include_status_events=True adds status/failed events to the iterable list; + # without it they are only accessible via result.status_timeline(). + result = await workflow.run( + "The quick brown fox jumps over the lazy dog.", + include_status_events=True, + ) for event in result: if event.type == "output": @@ -263,7 +268,7 @@ class TimingMiddleware(AgentMiddleware): elapsed = time.perf_counter() - context.metadata["start_time"] print(f"Elapsed: {elapsed:.3f}s") if not context.stream: - print(f"Tokens used: {context.result.usage}") + print(f"Tokens used: {context.result.usage_details}") async def main(): @@ -311,7 +316,7 @@ class MockMiddleware(AgentMiddleware): async def process(self, context: AgentContext, call_next): # Skip call_next entirely — return canned response context.result = AgentResponse( - message=Message.from_assistant(self._text), + messages=Message.from_assistant(self._text), ) ``` @@ -466,19 +471,18 @@ async def factual_check(item: EvalItem) -> CheckResult: async def main(): + # split_strategy belongs on EvalItem, not on evaluate() item = EvalItem( conversation=[ {"role": "user", "content": "What is the capital of France?"}, {"role": "assistant", "content": "The capital of France is Paris."}, - ] + ], + split_strategy=ConversationSplit.LAST_TURN, ) evaluator = LocalEvaluator(factual_check) - results = await evaluator.evaluate( - items=[item], - split=ConversationSplit.LAST_TURN, - ) + results = await evaluator.evaluate(items=[item]) for r in results.items: print(r.passed, r.reason) @@ -585,8 +589,8 @@ VectorStoreHistoryProvider( | Method | Signature | Notes | |---|---|---| -| `get_messages` | `async (session_id, *, state, **kwargs) → list[Message]` | Returns the full scoped transcript, sorted by creation time. | -| `save_messages` | `async (session_id, messages, *, state, **kwargs) → None` | Upserts new messages; assigns IDs to messages that lack one. | +| `get_messages` | `async (session_id, *, state=None, **kwargs) → list[Message]` | Returns the full scoped transcript, sorted by creation time. | +| `save_messages` | `async (session_id, messages, *, state=None, **kwargs) → None` | Upserts new messages; assigns IDs to messages that lack one. | | `clear` | `async (session_id) → None` | Deletes all records for the scoped history. | | `before_run` | `async (*, agent, session, context, state) → None` | Loads history, runs optional compaction, adds search tool. | @@ -736,7 +740,7 @@ MemoryFileStore( | `owner_prefix` | String prepended to the resolved owner ID for namespacing. | | `dumps` / `loads` | Custom JSON serialization hooks (defaults to `json.dumps` / `json.loads`). | -**Path resolution** follows `base_path / source_component / owner_component / kind`. Path traversal in owner IDs (`..", absolute paths) raises `ValueError`. +**Path resolution** follows `base_path / source_component / owner_component / kind`. Path traversal in owner IDs (`..`, absolute paths) raises `ValueError`. ### Example — file-backed memory with `MemoryContextProvider` @@ -1376,7 +1380,8 @@ async def main(): ) result = await agent.run("autumn leaves") - haiku = Haiku.model_validate_json(result.text) + # result.value holds the validated Pydantic model when response_format is set + haiku: Haiku = result.value print(f"{haiku.line1} / {haiku.line2} / {haiku.line3}") asyncio.run(main()) @@ -1385,7 +1390,8 @@ asyncio.run(main()) ### Example — type-safe option forwarding ```python -from typing import Unpack +# typing.Unpack requires Python 3.11+; use typing_extensions on Python 3.10 +from typing_extensions import Unpack from agent_framework import Agent, ChatOptions from agent_framework.openai import OpenAIChatClient @@ -1426,7 +1432,7 @@ The 1.19.0 release refines several of the APIs deep-dived in this and prior volu ## See also -- [Python Comprehensive Guide](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/) — framework overview with 1.18.0 feature table +- [Python Comprehensive Guide](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/) — framework overview, verified against 1.19.0 - [Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/) — workflow visualization, file memory, background agents, tool approval - [Class Deep Dives Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/) — fan-in/out edges, functional workflows, checkpointing, MCP tools - [Class Deep Dives Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/) — workflow builder, compaction strategies, evaluation, inline skills, file access From 72edd2be3722f9dd0de32bfd958a00ca1d5e8719 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:02:06 +0000 Subject: [PATCH 04/22] fix(vol5): address third-round Codex review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven additional API accuracy fixes to code examples: - AgentResponse(messages=[...]) — wrap single Message in a list; messages= accepts Message | Sequence[Message] but list is safer - create_agent_hooks_middleware: replace plain function stub with a proper Interceptor subclass (agent_hooks package); plain callables do not satisfy the Interceptor protocol - factual_check EvalCheck: item.response is already a str (joined assistant text); was incorrectly iterating it as a list of messages - CheckResult: add required check_name= field - results.items loop: EvalItemResult has is_passed/scores, not passed/reason; print r.status, r.is_passed, and r.scores entries - Custom ConversationSplitter comment: split_strategy goes on EvalItem, not as a split= kwarg on evaluate() - MemoryContextProvider: remove unsupported memory_agent= kwarg; use consolidation_client= instead Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index a054fcfb..71a6811a 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -316,7 +316,7 @@ class MockMiddleware(AgentMiddleware): async def process(self, context: AgentContext, call_next): # Skip call_next entirely — return canned response context.result = AgentResponse( - messages=Message.from_assistant(self._text), + messages=[Message.from_assistant(self._text)], ) ``` @@ -397,17 +397,23 @@ from agent_framework import Agent, acknowledge_experimental_feature, Experimenta from agent_framework import create_agent_hooks_middleware from agent_framework.openai import OpenAIChatClient +# agent_hooks Interceptor objects come from the agent-hooks package. +# Install it: pip install agent-hooks +from agent_hooks import Interceptor, InterceptionContext, Verdict + acknowledge_experimental_feature(ExperimentalFeature.AGENT_HOOKS) -# An interceptor is any callable matching the Interceptor protocol. -def my_interceptor(event_type: str, payload: dict) -> dict: - print(f"[Hook] event={event_type!r}") - return payload +class LoggingInterceptor(Interceptor): + """Log each interception point and allow all through.""" + + def intercept(self, context: InterceptionContext) -> Verdict: + print(f"[Hook] point={context.point!r} agent={context.agent_id!r}") + return Verdict.allow() bundle = create_agent_hooks_middleware( - interceptors=[my_interceptor], + interceptors=[LoggingInterceptor()], ) agent = Agent( @@ -460,14 +466,15 @@ acknowledge_experimental_feature(ExperimentalFeature.EVALS) # An EvalCheck is a callable: (EvalItem) -> CheckResult +# item.response is already a str (the joined assistant text from the response split). async def factual_check(item: EvalItem) -> CheckResult: """Pass if the response contains 'Paris'.""" - response_text = " ".join( - m.get("content", "") if isinstance(m, dict) else (m.text or "") - for m in item.response + passed = "paris" in item.response.lower() + return CheckResult( + check_name="factual_check", + passed=passed, + reason="Response mentions Paris" if passed else "Missing 'Paris'", ) - passed = "paris" in response_text.lower() - return CheckResult(passed=passed, reason="Response mentions Paris" if passed else "Missing 'Paris'") async def main(): @@ -484,7 +491,10 @@ async def main(): results = await evaluator.evaluate(items=[item]) for r in results.items: - print(r.passed, r.reason) + # r.is_passed: bool; r.scores: list[EvalScoreResult] with per-check detail + print(r.status, r.is_passed) + for score in r.scores: + print(f" check={score.name!r} passed={score.passed}") asyncio.run(main()) ``` @@ -508,8 +518,9 @@ def split_before_tool_call( return ConversationSplit.LAST_TURN(conversation) -# Use exactly like a built-in split: -# evaluator.evaluate(items=items, split=split_before_tool_call) +# Pass as split_strategy= on EvalItem, not as an argument to evaluate(): +# item = EvalItem(conversation=..., split_strategy=split_before_tool_call) +# results = await evaluator.evaluate(items=[item]) ``` ### Example — `FULL` split for trajectory evaluation @@ -761,13 +772,11 @@ async def main(): owner_state_key="user_id", ) + # MemoryContextProvider uses consolidation_client for the model that writes memories; + # it does not accept a separate memory_agent argument. provider = MemoryContextProvider( store=store, - memory_agent=Agent( - client=OpenAIChatClient(), - name="memory-agent", - instructions="Consolidate and maintain the user's long-term memories.", - ), + consolidation_client=OpenAIChatClient(), ) agent = Agent( From 26cd459caad6a9f827b5f5e9d38048bad759a29f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:08:38 +0000 Subject: [PATCH 05/22] fix(vol5): address fourth-round Codex review findings Three more corrections: - EvalItem.conversation expects list[Message], not raw dicts; replaced dict literals with Message('user', [...]) and Message('assistant', [...]) constructors; added Message import - agent-hooks-sdk install comment: correct package name is agent-hooks-sdk (pip install --pre agent-hooks-sdk) - HITL detecting-pending-requests example: collect all pending answers into one responses={} map before calling workflow.run() once; looping with one request per run() call restarts from the same checkpoint on each iteration, dropping earlier answers Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 71a6811a..82faa2d9 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -397,8 +397,8 @@ from agent_framework import Agent, acknowledge_experimental_feature, Experimenta from agent_framework import create_agent_hooks_middleware from agent_framework.openai import OpenAIChatClient -# agent_hooks Interceptor objects come from the agent-hooks package. -# Install it: pip install agent-hooks +# agent_hooks Interceptor objects come from the agent-hooks-sdk package. +# Install it: pip install --pre agent-hooks-sdk from agent_hooks import Interceptor, InterceptionContext, Verdict acknowledge_experimental_feature(ExperimentalFeature.AGENT_HOOKS) @@ -458,7 +458,7 @@ def my_splitter( import asyncio from agent_framework import ( Agent, EvalItem, EvalCheck, CheckResult, LocalEvaluator, ConversationSplit, - acknowledge_experimental_feature, ExperimentalFeature, + Message, acknowledge_experimental_feature, ExperimentalFeature, ) from agent_framework.openai import OpenAIChatClient @@ -478,11 +478,12 @@ async def factual_check(item: EvalItem) -> CheckResult: async def main(): + # EvalItem.conversation expects list[Message], not raw dicts # split_strategy belongs on EvalItem, not on evaluate() item = EvalItem( conversation=[ - {"role": "user", "content": "What is the capital of France?"}, - {"role": "assistant", "content": "The capital of France is Paris."}, + Message("user", ["What is the capital of France?"]), + Message("assistant", ["The capital of France is Paris."]), ], split_strategy=ConversationSplit.LAST_TURN, ) @@ -1096,14 +1097,18 @@ from agent_framework import WorkflowRunState async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): result = await workflow.run(initial_prompt) - if result.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS: - for req_event in result.get_request_info_events(): + # Collect ALL pending answers before resuming; each workflow.run(responses=...) + # call restarts from the stored checkpoint, so all answers must go in one map. + while result.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS: + pending = result.get_request_info_events() + responses = {} + for req_event in pending: print(f"Workflow is asking ({req_event.source_executor_id}): {req_event.data}") - answer = input("Your answer: ") - result = await workflow.run( - responses={req_event.request_id: answer}, - checkpoint_id=checkpoint_id, - ) + responses[req_event.request_id] = input("Your answer: ") + result = await workflow.run( + responses=responses, + checkpoint_id=checkpoint_id, + ) return result.get_outputs() ``` From e42991f4dee0eead02650011053c691dbc27a6a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:10:46 +0000 Subject: [PATCH 06/22] fix(vol5): add security disclaimer to argument pattern-matching example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SqlInjectionGuard example incorrectly presented a substring blocklist as SQL injection protection. A blocklist only covers a tiny subset of payloads and must never be the primary defence; real SQL injection protection requires parameterized queries. Changes: - Rename SqlInjectionGuard → ToolInputPatternGuard - Add prominent security note explaining the example is illustrative pattern-matching, not an injection guard - Update the error message to not imply SQL injection semantics Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...crosoft_agent_framework_python_class_deep_dives_v5.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 82faa2d9..752ee029 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -1183,13 +1183,16 @@ class ToolAuditMiddleware(FunctionMiddleware): print(f"[Audit] {context.function.name} → {context.result!r}") ``` -### Example — argument sanitisation middleware +### Example — argument pattern-matching middleware + +> **Security note:** A substring blocklist is **not** a reliable SQL injection guard — it covers only a small subset of payloads, ignores nested structures and encoding variants, and should never be the primary defence. Real SQL injection protection requires parameterized queries (e.g. SQLAlchemy bound parameters). The example below shows how to use function middleware to inspect and reject tool arguments by pattern, which is useful for logging, rate-limiting, or format validation — not as a security boundary. ```python from agent_framework import FunctionMiddleware, FunctionInvocationContext, MiddlewareTermination -class SqlInjectionGuard(FunctionMiddleware): +class ToolInputPatternGuard(FunctionMiddleware): + """Illustrative example: block tool calls containing specific substrings.""" BLOCKED = {"'; drop table", "union select", "--"} async def process(self, context: FunctionInvocationContext, call_next): @@ -1201,7 +1204,7 @@ class SqlInjectionGuard(FunctionMiddleware): if isinstance(value, str): lower = value.lower() if any(bad in lower for bad in self.BLOCKED): - raise MiddlewareTermination("Blocked: potential SQL injection.") + raise MiddlewareTermination("Input blocked by pattern guard.") await call_next() ``` From 2794afde118a2ec68fd8341dba401366316505d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:16:52 +0000 Subject: [PATCH 07/22] docs(index): update Python landing page to 1.19.0 and add Vol. 5 link - Bump version references from 1.18.0 to 1.19.0 in frontmatter description, Version card, and "What's shipped" section - Add Vol. 5 (1.19.0) LinkCard to the Reference section after Vol. 4 - Add 2026-09-21 / 1.19.0 entry to the revision history table Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- .../docs/microsoft-agent-framework-guide/python/index.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/index.mdx b/src/content/docs/microsoft-agent-framework-guide/python/index.mdx index 45e95571..984347f3 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/index.mdx +++ b/src/content/docs/microsoft-agent-framework-guide/python/index.mdx @@ -1,6 +1,6 @@ --- title: "Microsoft Agent Framework (Python)" -description: "Unified Python agent SDK — core stable at agent-framework 1.18.0, with a consolidated Class & API Reference covering 200+ classes across workflows, agents, memory, tools, security, skills, and more." +description: "Unified Python agent SDK — core stable at agent-framework 1.19.0, with a consolidated Class & API Reference covering 200+ classes across workflows, agents, memory, tools, security, skills, and more." framework: microsoft-agent-framework language: python sidebar: @@ -19,7 +19,7 @@ import { Card, CardGrid, LinkCard, Aside } from '@astrojs/starlight/components'; ``` - Core **1.18.0** stable · `agent-framework-foundry` and `agent-framework-openai` bundled via `agent-framework==1.18.0` · September 2026 · Python 3.10 – 3.13 · consolidated **Class & API Reference** + Core **1.19.0** stable · `agent-framework-foundry` and `agent-framework-openai` bundled via `agent-framework==1.19.0` · September 2026 · Python 3.10 – 3.13 · consolidated **Class & API Reference** Azure-native agents, multi-agent orchestration via `WorkflowBuilder`, cross-framework A2A, declarative YAML agents. @@ -118,11 +118,12 @@ A guided reading order across the comprehensive guide, 2025 features, advanced a + ## What's shipped (September 2026 introspection) -- **Stable core** — `agent-framework-core 1.18.0` (September 2026). The full class-by-class API surface — previously spread across 49 per-volume "class deep dive" pages — is now consolidated into one [Class & API Reference](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/#class--api-reference) section of the comprehensive guide (plus an azure-ai-agents add-on appendix). See the [Revision history](#revision-history) below for the version-by-version summary. +- **Stable core** — `agent-framework-core 1.19.0` (September 2026). The full class-by-class API surface — previously spread across 49 per-volume "class deep dive" pages — is now consolidated into one [Class & API Reference](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/#class--api-reference) section of the comprehensive guide (plus an azure-ai-agents add-on appendix). See the [Revision history](#revision-history) below for the version-by-version summary. - **First-party chat clients** — `agent_framework.foundry.FoundryChatClient`, `agent_framework.openai.OpenAIChatClient`, `agent_framework.anthropic.AnthropicClient`, plus Bedrock / Ollama in the `1.0.0b` provider line. - **Skills (experimental)** — `MemoryStore`, `SkillResource`, `InlineSkillResource`, `ClassSkill`, `InlineSkill`, `FileSkill` now emit `ExperimentalWarning` on import — do not depend on these APIs in production yet. - **Middleware** — `@chat_middleware` / `@agent_middleware` / `@function_middleware` decorators and base classes; pass `middleware=[...]` (must be a list in 2026 releases). @@ -169,6 +170,7 @@ Ready for the full walk-through? **[Start with Core Fundamentals →](/microsoft | Date | Version | Changes | |------|---------|---------| +| 2026-09-21 | 1.19.0 | Installed and introspected `agent-framework==1.19.0` source. Added [10-API Deep Dives Vol. 5](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5/) covering WorkflowEvent, AgentContext, MiddlewareBundle, ConversationSplit/ConversationSplitter, VectorStoreHistoryProvider, MemoryStore/MemoryFileStore, MemoryTopicRecord, WorkflowRunResult, FunctionInvocationContext, and ChatOptions — all with source-verified constructors, method tables, and runnable examples. Vol. 5 LinkCard added to Reference section. Version card and description updated to 1.19.0. | | 2026-09-14 | 1.18.0 | Installed and introspected `agent-framework==1.18.0` source. Added [10-API Deep Dives Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/) covering eight classes (VectorStoreField, VectorStoreCollectionDefinition, InMemoryCollection, InMemoryStore, Filter, FilterGroup, SecretString, GroupChatBuilder) and two functions (load_settings, create_agent_hooks_middleware) — all with source-verified constructors, method tables, and runnable examples. Vol. 2 and Vol. 3 LinkCards added to Reference section. Version card updated to 1.18.0. | | 2026-09-08 | 1.17.0 | Consistency sweep. Version card and page description moved 1.15.0 → 1.17.0 to match the newest source-verified deep dives (Vol. 3, `agent-framework==1.17.0`); comprehensive-guide header now states agent-framework 1.17.0 as latest and 1.14.0 as the verified-against version. Zero → Hero deep links into the 2025-features page retargeted to headings that exist. No package installation or symbol verification performed. | | 2026-08-24 | 1.15.0 | Upgraded to agent-framework 1.15.0 (latest). Added [10-Class Deep Dives](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/) document covering WorkflowViz, FileMemoryProvider, AgentModeProvider, BackgroundAgentsProvider, ToolApprovalMiddleware, SwitchCaseEdgeGroup, MessageInjectionMiddleware, ToolResultCompactionStrategy, SummarizationStrategy, and TokenBudgetComposedStrategy — all source-verified with runnable examples. | From 63e45811a175b46a228f8ef5cfebbd855ca36fa3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:23:30 +0000 Subject: [PATCH 08/22] fix(vol5): address four Copilot review findings - Fix VectorStoreHistoryProvider comment: both runs use the same session, not a "later session" (line 685) - Fix MemoryTopicRecord custom-store stub: key write_topic by record.topic (not record.slug) so get_topic/delete_topic lookups match (line 832) - Fix WorkflowRunResult executor filter: add id="writer" to Agent so that ev.executor_id == "writer" matches the assigned ID, not an auto-generated one (line 1066) - Fix ChatOptions store field description: it controls provider-side conversation storage (OpenAI Responses API, Foundry), not fine-tuning persistence (line 1306) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...icrosoft_agent_framework_python_class_deep_dives_v5.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 752ee029..11378dad 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -681,7 +681,7 @@ async def main(): session = agent.create_session() await agent.run("My favourite colour is blue.", session=session) - # In a later session the agent can search for "favourite colour" + # Later in the same session the agent can search for "favourite colour" result = await agent.run("What did I say about colours?", session=session) print(result.text) @@ -829,7 +829,7 @@ class InMemoryMemoryStore(MemoryStore): return record def write_topic(self, session, record, *, source_id): - self._topics.setdefault(self._owner(session), {})[record.slug] = record + self._topics.setdefault(self._owner(session), {})[record.topic] = record def delete_topic(self, session, *, source_id, topic): self._topics.get(self._owner(session), {}).pop(topic, None) @@ -1063,7 +1063,7 @@ async def main(): client = OpenAIChatClient() researcher = Agent(client=client, name="researcher", instructions="Research the given topic and write 3 key facts.") - writer = Agent(client=client, name="writer", + writer = Agent(client=client, id="writer", name="writer", instructions="Turn the researcher's facts into a polished paragraph.") workflow = ( @@ -1303,7 +1303,7 @@ class TraceIdMiddleware(AgentMiddleware): | `response_format` | `type[BaseModel] \| Mapping[str, Any] \| None` | Structured output schema. | | `metadata` | `dict[str, Any]` | Provider-specific metadata. | | `user` | `str` | End-user identifier (e.g. for OpenAI abuse monitoring). | -| `store` | `bool` | Whether to persist the request for fine-tuning (OpenAI). | +| `store` | `bool` | Whether to persist the conversation server-side (provider-specific; e.g. OpenAI Responses API, Foundry). | | `conversation_id` | `str` | Conversation identifier (provider-specific). | | `instructions` | `str` | System-level instructions override. | From 21dd9fce515cd5d384526bcee44a3bd0644961db Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:31:20 +0000 Subject: [PATCH 09/22] fix(vol5): fix interceptor context access and memory store isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix LoggingInterceptor: InterceptionContext is a mapping; access data via context.get('interception_point') and context.get('agent_id') rather than attribute access (line 412) - Fix InMemoryMemoryStore: partition storage by (source_id, owner) via _key() helper so two MemoryContextProviders for the same user cannot overwrite each other's data (line 816) - Fix InMemoryMemoryStore: maintain a slug→topic index so get_topic() and delete_topic() succeed whether called with a display name or a slug (line 832) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 11378dad..bfaf9777 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -408,7 +408,8 @@ class LoggingInterceptor(Interceptor): """Log each interception point and allow all through.""" def intercept(self, context: InterceptionContext) -> Verdict: - print(f"[Hook] point={context.point!r} agent={context.agent_id!r}") + # InterceptionContext is a mapping; access data via .get() + print(f"[Hook] point={context.get('interception_point')!r} agent={context.get('agent_id')!r}") return Verdict.allow() @@ -811,28 +812,39 @@ class InMemoryMemoryStore(MemoryStore): """In-memory MemoryStore for unit testing.""" def __init__(self): - self._topics: dict[str, dict[str, MemoryTopicRecord]] = {} # owner → slug → record - self._states: dict[str, dict] = {} + # Outer key: (source_id, owner) — isolates each provider instance per user + self._topics: dict[tuple[str, str], dict[str, MemoryTopicRecord]] = {} # topic → record + self._slug_idx: dict[tuple[str, str], dict[str, str]] = {} # slug → topic + self._states: dict[tuple[str, str], dict] = {} self._tmp = Path("/tmp/in-memory-store-transcripts") - def _owner(self, session: AgentSession) -> str: - return str(session.state.get("user_id", "default")) + def _key(self, session: AgentSession, source_id: str) -> tuple[str, str]: + return (source_id, str(session.state.get("user_id", "default"))) def list_topics(self, session, *, source_id): - return sorted(self._topics.get(self._owner(session), {}).values(), + return sorted(self._topics.get(self._key(session, source_id), {}).values(), key=lambda r: r.topic) def get_topic(self, session, *, source_id, topic): - record = self._topics.get(self._owner(session), {}).get(topic) + key = self._key(session, source_id) + # resolve slug to canonical topic name if needed + resolved = self._slug_idx.get(key, {}).get(topic, topic) + record = self._topics.get(key, {}).get(resolved) if record is None: raise FileNotFoundError(topic) return record def write_topic(self, session, record, *, source_id): - self._topics.setdefault(self._owner(session), {})[record.topic] = record + key = self._key(session, source_id) + self._topics.setdefault(key, {})[record.topic] = record + self._slug_idx.setdefault(key, {})[record.slug] = record.topic def delete_topic(self, session, *, source_id, topic): - self._topics.get(self._owner(session), {}).pop(topic, None) + key = self._key(session, source_id) + resolved = self._slug_idx.get(key, {}).get(topic, topic) + rec = self._topics.get(key, {}).pop(resolved, None) + if rec is not None: + self._slug_idx.get(key, {}).pop(rec.slug, None) def rebuild_index(self, session, *, source_id, line_limit, line_length): return [MemoryIndexEntry.from_topic_record(t) for t in self.list_topics(session, source_id=source_id)] @@ -843,10 +855,10 @@ class InMemoryMemoryStore(MemoryStore): return "\n".join(e.to_pointer_line(max_length=line_length) for e in entries) def read_state(self, session, *, source_id): - return dict(self._states.get(self._owner(session), {})) + return dict(self._states.get(self._key(session, source_id), {})) def write_state(self, session, state, *, source_id): - self._states[self._owner(session)] = dict(state) + self._states[self._key(session, source_id)] = dict(state) def get_transcripts_directory(self, session, *, source_id): self._tmp.mkdir(parents=True, exist_ok=True) From 766616b62c1bd3bda27ab683753178ebd1094e30 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:36:50 +0000 Subject: [PATCH 10/22] fix(vol5): scope transcript dir by source+owner; non-blocking input - Fix get_transcripts_directory(): derive a subdirectory from (source_id, owner) so multiple providers/owners cannot write colliding transcript files (line 863) - Fix HITL examples: replace synchronous input() with await asyncio.to_thread(input, ...) to avoid blocking the event loop while waiting for user input (lines 183 and 1119) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...soft_agent_framework_python_class_deep_dives_v5.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index bfaf9777..6cc1e26f 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -180,7 +180,7 @@ async def handle_pending_request(run_result, workflow, checkpoint_id: str): req: WorkflowEvent = pending[0] # req.request_id, req.source_executor_id, req.data, req.response_type print(f"Workflow is asking: {req.data}") - user_answer = input("Your answer: ") + user_answer = await asyncio.to_thread(input, "Your answer: ") resumed = await workflow.run( responses={req.request_id: user_answer}, @@ -861,8 +861,11 @@ class InMemoryMemoryStore(MemoryStore): self._states[self._key(session, source_id)] = dict(state) def get_transcripts_directory(self, session, *, source_id): - self._tmp.mkdir(parents=True, exist_ok=True) - return self._tmp + owner = self._key(session, source_id)[1] + # Scope by source_id and owner so transcript files don't collide + scoped = self._tmp / source_id.replace("/", "_") / owner + scoped.mkdir(parents=True, exist_ok=True) + return scoped def search_transcripts(self, session, *, source_id, query, session_id=None, limit=20): return [] @@ -1116,7 +1119,7 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): responses = {} for req_event in pending: print(f"Workflow is asking ({req_event.source_executor_id}): {req_event.data}") - responses[req_event.request_id] = input("Your answer: ") + responses[req_event.request_id] = await asyncio.to_thread(input, "Your answer: ") result = await workflow.run( responses=responses, checkpoint_id=checkpoint_id, From da6cc2f9cb0e4853a5c539ac1692e92be4af6bc6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:44:04 +0000 Subject: [PATCH 11/22] fix(vol5): streaming timing, owner id, path safety, HITL checkpoint - Fix TimingMiddleware: guard elapsed/token print behind !context.stream; for streaming runs explain that elapsed measures only stream setup (line 269) - Fix InMemoryMemoryStore: override get_owner_id() so export/import can restore the correct user bucket on reconstructed sessions (line 822) - Fix InMemoryMemoryStore transcript path: sanitize source_id and owner via _safe() to prevent path traversal when user_id contains separators or ".." (line 867) - Fix HITL loop: update checkpoint_id from result.checkpoint_id after each round so subsequent responses resume from the latest checkpoint, not the original one (line 1126) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 6cc1e26f..30a47b30 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -266,9 +266,15 @@ class TimingMiddleware(AgentMiddleware): await call_next() elapsed = time.perf_counter() - context.metadata["start_time"] - print(f"Elapsed: {elapsed:.3f}s") if not context.stream: + # Non-streaming: elapsed covers the full model round-trip. + print(f"Elapsed: {elapsed:.3f}s") print(f"Tokens used: {context.result.usage_details}") + else: + # Streaming: call_next() returns a ResponseStream quickly; elapsed + # measures stream setup only. Measure inside the consumer loop for + # accurate generation latency. + print(f"Stream ready in {elapsed:.3f}s (measure generation in the consumer)") async def main(): @@ -803,11 +809,17 @@ asyncio.run(main()) ### Example — custom `MemoryStore` implementation ```python +import re from pathlib import Path from agent_framework import AgentSession from agent_framework._harness._memory import MemoryStore, MemoryTopicRecord, MemoryIndexEntry +def _safe(s: str) -> str: + """Sanitize a string for use as a path component.""" + return re.sub(r'[^a-zA-Z0-9._-]', '_', s)[:64] or 'default' + + class InMemoryMemoryStore(MemoryStore): """In-memory MemoryStore for unit testing.""" @@ -821,6 +833,9 @@ class InMemoryMemoryStore(MemoryStore): def _key(self, session: AgentSession, source_id: str) -> tuple[str, str]: return (source_id, str(session.state.get("user_id", "default"))) + def get_owner_id(self, session: AgentSession) -> str: + return str(session.state.get("user_id", "default")) + def list_topics(self, session, *, source_id): return sorted(self._topics.get(self._key(session, source_id), {}).values(), key=lambda r: r.topic) @@ -862,8 +877,8 @@ class InMemoryMemoryStore(MemoryStore): def get_transcripts_directory(self, session, *, source_id): owner = self._key(session, source_id)[1] - # Scope by source_id and owner so transcript files don't collide - scoped = self._tmp / source_id.replace("/", "_") / owner + # Sanitize both components to prevent path traversal + scoped = self._tmp / _safe(source_id) / _safe(owner) scoped.mkdir(parents=True, exist_ok=True) return scoped @@ -1124,6 +1139,8 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): responses=responses, checkpoint_id=checkpoint_id, ) + # Advance to the new checkpoint written by this round + checkpoint_id = result.checkpoint_id return result.get_outputs() ``` From 00dcb808a706c55e9b393e0675c57b5d58751774 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:49:30 +0000 Subject: [PATCH 12/22] fix(vol5): base64 path encoding, provider state export, HITL checkpoint - Fix _safe(): use URL-safe base64 (injective, no collisions) instead of regex substitution which allowed '.' to pass through, enabling '..' path traversal and collisions between distinct IDs (line 820) - Fix InMemoryMemoryStore: add export_provider_state() and import_provider_state() to serialize and restore user_id so reconstructed sessions select the correct owner bucket (line 837) - Fix HITL loop: remove result.checkpoint_id (attribute does not exist on WorkflowRunResult); checkpoint storage updates in-place so the same checkpoint_id is valid across all rounds (line 1143) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...agent_framework_python_class_deep_dives_v5.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 30a47b30..35ab0241 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -809,15 +809,15 @@ asyncio.run(main()) ### Example — custom `MemoryStore` implementation ```python -import re +import base64 from pathlib import Path from agent_framework import AgentSession from agent_framework._harness._memory import MemoryStore, MemoryTopicRecord, MemoryIndexEntry def _safe(s: str) -> str: - """Sanitize a string for use as a path component.""" - return re.sub(r'[^a-zA-Z0-9._-]', '_', s)[:64] or 'default' + """URL-safe base64 encode a string for use as a path component (injective, no collisions).""" + return base64.urlsafe_b64encode(s.encode()).decode().rstrip('=') class InMemoryMemoryStore(MemoryStore): @@ -836,6 +836,12 @@ class InMemoryMemoryStore(MemoryStore): def get_owner_id(self, session: AgentSession) -> str: return str(session.state.get("user_id", "default")) + def export_provider_state(self, session: AgentSession, *, source_id: str) -> dict: + return {"user_id": str(session.state.get("user_id", "default"))} + + def import_provider_state(self, session: AgentSession, state: dict, *, source_id: str) -> None: + session.state["user_id"] = state.get("user_id", "default") + def list_topics(self, session, *, source_id): return sorted(self._topics.get(self._key(session, source_id), {}).values(), key=lambda r: r.topic) @@ -1139,8 +1145,8 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): responses=responses, checkpoint_id=checkpoint_id, ) - # Advance to the new checkpoint written by this round - checkpoint_id = result.checkpoint_id + # The checkpoint storage updates in-place; the same checkpoint_id + # is valid for every subsequent round. return result.get_outputs() ``` From 873701d981f9916a5fee263271e64663d6223f65 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:50:49 +0000 Subject: [PATCH 13/22] docs(vol5): clarify WorkflowRunResult iterable and fix API count - Qualify WorkflowRunResult list description: by default data-plane only; with include_status_events=True status events are also in the list - Update aggregate API count in comprehensive guide from 50 to 52 (50 classes + 2 functions, counting ConversationSplit/ConversationSplitter and MemoryStore/MemoryFileStore as distinct types each) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- .../microsoft_agent_framework_python_class_deep_dives_v5.md | 2 +- .../microsoft_agent_framework_python_comprehensive_guide.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 35ab0241..d859ec61 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -1017,7 +1017,7 @@ def find_topics_matching(store: MemoryFileStore, session: AgentSession, keyword: **Module:** `agent_framework._workflows._workflow` (re-exported via `agent_framework`) -`WorkflowRunResult` is a `list[WorkflowEvent]` subclass returned by `await workflow.run(...)`. It holds the **data-plane** events (executor invocations, completions, outputs, and `request_info` pauses) in the list itself, and the **control-plane** status events in a separate private list accessible via `status_timeline()`. +`WorkflowRunResult` is a `list[WorkflowEvent]` subclass returned by `await workflow.run(...)`. By default the list holds only **data-plane** events (executor invocations, completions, outputs, and `request_info` pauses); control-plane status events are kept in a separate private list accessible via `status_timeline()`. When `workflow.run(..., include_status_events=True)` is passed, status and failed events are also included in the list itself alongside data-plane events. ### Constructor diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md index 65b65868..a8f3a577 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide.md @@ -14,7 +14,7 @@ Latest: agent-framework 1.19.0 | Guide verified against: 1.14.0 | Python 3.10+ --- -> **API reference (verified against `agent-framework==1.14.0`; 50 APIs (48 classes and 2 functions) verified on 1.15.0–1.19.0 — see [10-Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/), [Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/), [Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/), [Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/) and [Vol. 5](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5/)).** +> **API reference (verified against `agent-framework==1.14.0`; 52 APIs (50 classes and 2 functions) verified on 1.15.0–1.19.0 — see [10-Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/), [Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/), [Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/), [Vol. 4](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v4/) and [Vol. 5](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5/)).** > > - **Package name / import root:** `agent_framework` (underscores). Install with `pip install agent-framework`. > - **Agent classes:** `Agent` (full stack with middleware + telemetry), `RawAgent` (same interface, skips the middleware/telemetry wrappers for latency-sensitive paths), `BaseAgent` (abstract base for custom subclasses). From ae43ba1593ae4f1d93610f29c94f910faaf0ebce Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:55:28 +0000 Subject: [PATCH 14/22] fix(vol5): correct export/import_provider_state method signatures The MemoryStore contract calls export_provider_state(session) and import_provider_state(session, *, state) without source_id; remove the erroneous source_id parameter from both overrides so wiring into MemoryContextProvider doesn't raise TypeError. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- .../microsoft_agent_framework_python_class_deep_dives_v5.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index d859ec61..4c63bf89 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -836,10 +836,10 @@ class InMemoryMemoryStore(MemoryStore): def get_owner_id(self, session: AgentSession) -> str: return str(session.state.get("user_id", "default")) - def export_provider_state(self, session: AgentSession, *, source_id: str) -> dict: + def export_provider_state(self, session: AgentSession) -> dict: return {"user_id": str(session.state.get("user_id", "default"))} - def import_provider_state(self, session: AgentSession, state: dict, *, source_id: str) -> None: + def import_provider_state(self, session: AgentSession, *, state: dict) -> None: session.state["user_id"] = state.get("user_id", "default") def list_topics(self, session, *, source_id): From 966e18399e8dbef9cd48eafbf0f456f2b89a4dc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 10:58:44 +0000 Subject: [PATCH 15/22] fix(v5): refresh checkpoint_id via get_latest() in HITL loop Each resumed workflow.run() saves a fresh checkpoint forming a previous_checkpoint_id chain rather than updating in-place. Query checkpoint_storage.get_latest(workflow_name=workflow.name) before every iteration to obtain the current checkpoint_id. Also pass checkpoint_storage to the initial workflow.run() call and change the function signature to accept checkpoint_storage instead of a raw checkpoint_id string. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...t_agent_framework_python_class_deep_dives_v5.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 4c63bf89..2eeda08e 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -1130,12 +1130,13 @@ import asyncio from agent_framework import WorkflowRunState -async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): - result = await workflow.run(initial_prompt) +async def run_and_handle(workflow, initial_prompt: str, checkpoint_storage): + result = await workflow.run(initial_prompt, checkpoint_storage=checkpoint_storage) - # Collect ALL pending answers before resuming; each workflow.run(responses=...) - # call restarts from the stored checkpoint, so all answers must go in one map. + # Each resumed run saves a FRESH checkpoint forming a previous_checkpoint_id + # chain, so refresh the checkpoint_id via get_latest() before every iteration. while result.get_final_state() == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS: + latest = await checkpoint_storage.get_latest(workflow_name=workflow.name) pending = result.get_request_info_events() responses = {} for req_event in pending: @@ -1143,10 +1144,9 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_id: str): responses[req_event.request_id] = await asyncio.to_thread(input, "Your answer: ") result = await workflow.run( responses=responses, - checkpoint_id=checkpoint_id, + checkpoint_id=latest.checkpoint_id, + checkpoint_storage=checkpoint_storage, ) - # The checkpoint storage updates in-place; the same checkpoint_id - # is valid for every subsequent round. return result.get_outputs() ``` From 18a39eef8cac8b63aa4cdbda31085d4e9da407ca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:04:47 +0000 Subject: [PATCH 16/22] fix(v5): scope checkpoint storage per run to prevent cross-contamination get_latest(workflow_name=...) selects the newest checkpoint for that name in a given store; two concurrent invocations sharing the same store can pick up each other's checkpoints. Add a comment explaining the single-run-per-storage requirement, and add a handle_request() usage pattern that creates a fresh MemoryFileStore scoped to a uuid run_id for each invocation. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ..._agent_framework_python_class_deep_dives_v5.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 2eeda08e..6f9238bc 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -1127,10 +1127,17 @@ asyncio.run(main()) ```python import asyncio +import uuid from agent_framework import WorkflowRunState +from agent_framework.checkpointing import MemoryFileStore async def run_and_handle(workflow, initial_prompt: str, checkpoint_storage): + # IMPORTANT: checkpoint_storage must be scoped to a single run. + # get_latest(workflow_name=...) selects the newest checkpoint for that + # name in the given store — if two concurrent runs share the same store + # they can cross-contaminate. Callers should create a fresh, run-scoped + # storage instance for every invocation (see usage example below). result = await workflow.run(initial_prompt, checkpoint_storage=checkpoint_storage) # Each resumed run saves a FRESH checkpoint forming a previous_checkpoint_id @@ -1149,6 +1156,14 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_storage): ) return result.get_outputs() + + +# Usage: create a run-scoped storage so get_latest() only ever sees +# checkpoints from this specific invocation, even in multi-user deployments. +async def handle_request(workflow, prompt: str): + run_id = uuid.uuid4().hex + storage = MemoryFileStore(path=f"./checkpoints/{run_id}") + return await run_and_handle(workflow, prompt, storage) ``` --- From 820264fef5ca8c84cac55d301fdbf80c81cedaa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:09:57 +0000 Subject: [PATCH 17/22] fix(v5): use FileCheckpointStorage not MemoryFileStore for HITL example MemoryFileStore is the long-term-memory backend; workflow checkpoints use FileCheckpointStorage (re-exported from agent_framework). Replace the incorrect import and constructor call in the HITL handle_request() usage pattern. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...soft_agent_framework_python_class_deep_dives_v5.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 6f9238bc..70600cad 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -1128,8 +1128,7 @@ asyncio.run(main()) ```python import asyncio import uuid -from agent_framework import WorkflowRunState -from agent_framework.checkpointing import MemoryFileStore +from agent_framework import WorkflowRunState, FileCheckpointStorage async def run_and_handle(workflow, initial_prompt: str, checkpoint_storage): @@ -1137,7 +1136,7 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_storage): # get_latest(workflow_name=...) selects the newest checkpoint for that # name in the given store — if two concurrent runs share the same store # they can cross-contaminate. Callers should create a fresh, run-scoped - # storage instance for every invocation (see usage example below). + # FileCheckpointStorage instance for every invocation (see below). result = await workflow.run(initial_prompt, checkpoint_storage=checkpoint_storage) # Each resumed run saves a FRESH checkpoint forming a previous_checkpoint_id @@ -1158,11 +1157,11 @@ async def run_and_handle(workflow, initial_prompt: str, checkpoint_storage): return result.get_outputs() -# Usage: create a run-scoped storage so get_latest() only ever sees -# checkpoints from this specific invocation, even in multi-user deployments. +# Usage: create a run-scoped FileCheckpointStorage so get_latest() only ever +# sees checkpoints from this specific invocation, even in multi-user deployments. async def handle_request(workflow, prompt: str): run_id = uuid.uuid4().hex - storage = MemoryFileStore(path=f"./checkpoints/{run_id}") + storage = FileCheckpointStorage(f"./checkpoints/{run_id}") return await run_and_handle(workflow, prompt, storage) ``` From 57c0f47fb553923d4c1c0a9e3e998479e6e51eaa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:15:50 +0000 Subject: [PATCH 18/22] fix(v5): fix _safe() empty-string collision and honour line_limit in index _safe("") returned "" causing (source_id="", owner="x") and (source_id="x", owner="") to resolve to the same filesystem path. Prefix the base64 output with "s" so every input including "" maps to a non-empty, unique path component. rebuild_index() and get_index_text() both ignored the line_limit parameter, diverging from the production MemoryContextProvider's index bound. Slice topics and entries to [:line_limit] in both. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ..._agent_framework_python_class_deep_dives_v5.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 70600cad..58ac266c 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -816,8 +816,13 @@ from agent_framework._harness._memory import MemoryStore, MemoryTopicRecord, Mem def _safe(s: str) -> str: - """URL-safe base64 encode a string for use as a path component (injective, no collisions).""" - return base64.urlsafe_b64encode(s.encode()).decode().rstrip('=') + """URL-safe base64 encode a string for use as a path component (injective, no collisions). + + A constant 's' prefix ensures empty strings produce a non-empty component + ("s"), preventing (source_id="", owner="x") from colliding with + (source_id="x", owner="") under filesystem path joining. + """ + return "s" + base64.urlsafe_b64encode(s.encode()).decode().rstrip('=') class InMemoryMemoryStore(MemoryStore): @@ -868,12 +873,14 @@ class InMemoryMemoryStore(MemoryStore): self._slug_idx.get(key, {}).pop(rec.slug, None) def rebuild_index(self, session, *, source_id, line_limit, line_length): - return [MemoryIndexEntry.from_topic_record(t) for t in self.list_topics(session, source_id=source_id)] + topics = self.list_topics(session, source_id=source_id) + return [MemoryIndexEntry.from_topic_record(t) for t in topics[:line_limit]] def get_index_text(self, session, *, source_id, line_limit, line_length, index_entries=None): entries = index_entries or self.rebuild_index(session, source_id=source_id, line_limit=line_limit, line_length=line_length) - return "\n".join(e.to_pointer_line(max_length=line_length) for e in entries) + # Truncate to line_limit so tests match the production provider's index bound. + return "\n".join(e.to_pointer_line(max_length=line_length) for e in entries[:line_limit]) def read_state(self, session, *, source_id): return dict(self._states.get(self._key(session, source_id), {})) From b8fbc0af729d39327dff62b008bba02baca688e5 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:23:54 +0000 Subject: [PATCH 19/22] fix(v5): add deprecated data/emit event, bound _safe() length, fix guide version WorkflowEvent taxonomy was missing the deprecated 'data' event type produced by WorkflowEvent.emit(). Add the row to both the event-type table and the factory-method table with a deprecation note. _safe() had no length bound; IDs > ~190 UTF-8 bytes would produce a base64 component exceeding the 255-byte filesystem limit. Now falls back to 'd' + SHA-256 hex (64 chars) for long values, keeping 's' + base64 for short ones. Distinct prefixes prevent cross-scheme collisions. The See Also link to the comprehensive guide incorrectly claimed it was verified against 1.19.0; the guide's own header states 1.14.0. Updated the label to reflect both: content verified against 1.14.0, latest release badge 1.19.0. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...nt_framework_python_class_deep_dives_v5.md | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 58ac266c..2b970fe1 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -60,6 +60,7 @@ WorkflowEvent( | `"failed"` | `WorkflowEvent.failed(details)` | `None` / DataT | `details` | | `"warning"` | `WorkflowEvent.warning(msg)` | `str` | — | | `"error"` | `WorkflowEvent.error(exc)` | `Exception` | — | +| `"data"` | `WorkflowEvent.emit(data)` *(deprecated)* | DataT | — | | `"output"` | emitted by `ctx.yield_output()` | DataT | `executor_id` | | `"intermediate"` | emitted by `ctx.yield_output()` (intermediate) | DataT | `executor_id` | | `"request_info"` | `WorkflowEvent.request_info(...)` | DataT | `request_id`, `source_executor_id` | @@ -79,6 +80,7 @@ WorkflowEvent( | `failed` | `(details: WorkflowErrorDetails, data=None) → WorkflowEvent[DataT]` | Run termination | | `warning` | `(message: str) → WorkflowEvent[str]` | User-emitted diagnostic | | `error` | `(exception: Exception) → WorkflowEvent[Exception]` | User-emitted diagnostic | +| `emit` | `(data: DataT) → WorkflowEvent[DataT]` *(deprecated)* | Produces `"data"` events; prefer `ctx.yield_output()` | | `request_info` | `(request_id, source_executor_id, request_data, response_type) → WorkflowEvent[DataT]` | Human-in-the-loop pause | | `superstep_started` | `(iteration: int, data=None) → WorkflowEvent[DataT]` | Pregel superstep begin | | `superstep_completed` | `(iteration: int, data=None) → WorkflowEvent[DataT]` | Pregel superstep end | @@ -810,19 +812,25 @@ asyncio.run(main()) ```python import base64 +import hashlib from pathlib import Path from agent_framework import AgentSession from agent_framework._harness._memory import MemoryStore, MemoryTopicRecord, MemoryIndexEntry +_MAX_COMPONENT = 200 # comfortably below the 255-byte filesystem name limit + def _safe(s: str) -> str: - """URL-safe base64 encode a string for use as a path component (injective, no collisions). + """Encode a string as a safe filesystem path component (injective, length-bounded). - A constant 's' prefix ensures empty strings produce a non-empty component - ("s"), preventing (source_id="", owner="x") from colliding with - (source_id="x", owner="") under filesystem path joining. + Short values: 's' + url-safe-base64 (injective, no collisions including ""). + Long values: 'd' + SHA-256 hex (64 chars, collision-resistant for any practical ID). + The distinct prefixes ensure the two schemes never collide with each other. """ - return "s" + base64.urlsafe_b64encode(s.encode()).decode().rstrip('=') + encoded = "s" + base64.urlsafe_b64encode(s.encode()).decode().rstrip('=') + if len(encoded.encode()) > _MAX_COMPONENT: + return "d" + hashlib.sha256(s.encode()).hexdigest() + return encoded class InMemoryMemoryStore(MemoryStore): @@ -1508,7 +1516,7 @@ The 1.19.0 release refines several of the APIs deep-dived in this and prior volu ## See also -- [Python Comprehensive Guide](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/) — framework overview, verified against 1.19.0 +- [Python Comprehensive Guide](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_comprehensive_guide/) — framework overview, content verified against 1.14.0 (latest release badge: 1.19.0) - [Class Deep Dives Vol. 1](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives/) — workflow visualization, file memory, background agents, tool approval - [Class Deep Dives Vol. 2](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v2/) — fan-in/out edges, functional workflows, checkpointing, MCP tools - [Class Deep Dives Vol. 3](/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v3/) — workflow builder, compaction strategies, evaluation, inline skills, file access From 201c05b3c32d87f5c12f433d0364ccfe980f1202 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 21 Sep 2026 11:31:09 +0000 Subject: [PATCH 20/22] fix(v5): give each InMemoryMemoryStore instance its own temp directory All instances previously shared a single /tmp/in-memory-store-transcripts root, so parallel tests with matching source_id/owner/session IDs would write into the same transcript directory despite having independent in-memory state. Use tempfile.mkdtemp(prefix="in-memory-store-") in __init__ to give every instance a unique root, and add a cleanup() method that calls shutil.rmtree for teardown. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...crosoft_agent_framework_python_class_deep_dives_v5.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 2b970fe1..94c98a06 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -813,6 +813,8 @@ asyncio.run(main()) ```python import base64 import hashlib +import shutil +import tempfile from pathlib import Path from agent_framework import AgentSession from agent_framework._harness._memory import MemoryStore, MemoryTopicRecord, MemoryIndexEntry @@ -841,7 +843,12 @@ class InMemoryMemoryStore(MemoryStore): self._topics: dict[tuple[str, str], dict[str, MemoryTopicRecord]] = {} # topic → record self._slug_idx: dict[tuple[str, str], dict[str, str]] = {} # slug → topic self._states: dict[tuple[str, str], dict] = {} - self._tmp = Path("/tmp/in-memory-store-transcripts") + # Unique per-instance root so parallel test instances never share transcript dirs. + self._tmp = Path(tempfile.mkdtemp(prefix="in-memory-store-")) + + def cleanup(self) -> None: + """Remove the instance's temporary transcript directory.""" + shutil.rmtree(self._tmp, ignore_errors=True) def _key(self, session: AgentSession, source_id: str) -> tuple[str, str]: return (source_id, str(session.state.get("user_id", "default"))) From 83c741c5e5cb777088e81d7b5d5924fcc64164ee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 13:10:16 +0000 Subject: [PATCH 21/22] fix(maf-v5): make examples run on 1.19.0 and correct What's new table - Remove nonexistent acknowledge_experimental_feature; experimental features only emit a one-time ExperimentalWarning. - Replace nonexistent Message.from_* helpers with Message(role, [...]). - Fix EvalCheck and agent_hooks InterceptionContext imports. - WorkflowEvent.emit takes (executor_id, data); add missing event types. - AgentContext.options may be None. - Rewrite "What's new in 1.19.0" from a 1.18.0 vs 1.19.0 export diff: the vector-store providers, tool factories and ResponseInvalidatedException are new; the other listed changes were not. Verified against agent-framework 1.19.0: every runnable block imports and constructs cleanly (model calls reach the network). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UbuuyLymfkxVYM5kGzDV53 --- ...nt_framework_python_class_deep_dives_v5.md | 107 +++++++----------- 1 file changed, 44 insertions(+), 63 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 94c98a06..84836626 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -60,7 +60,7 @@ WorkflowEvent( | `"failed"` | `WorkflowEvent.failed(details)` | `None` / DataT | `details` | | `"warning"` | `WorkflowEvent.warning(msg)` | `str` | — | | `"error"` | `WorkflowEvent.error(exc)` | `Exception` | — | -| `"data"` | `WorkflowEvent.emit(data)` *(deprecated)* | DataT | — | +| `"data"` | `WorkflowEvent.emit(executor_id, data)` *(deprecated)* | DataT | `executor_id` | | `"output"` | emitted by `ctx.yield_output()` | DataT | `executor_id` | | `"intermediate"` | emitted by `ctx.yield_output()` (intermediate) | DataT | `executor_id` | | `"request_info"` | `WorkflowEvent.request_info(...)` | DataT | `request_id`, `source_executor_id` | @@ -70,6 +70,9 @@ WorkflowEvent( | `"executor_completed"` | `WorkflowEvent.executor_completed(id)` | `None` / DataT | `executor_id` | | `"executor_failed"` | `WorkflowEvent.executor_failed(id, details)` | `WorkflowErrorDetails` | `executor_id`, `details` | | `"executor_bypassed"` | `WorkflowEvent.executor_bypassed(id)` | `None` / DataT | `executor_id` — cache-hit replay | +| `"group_chat"` | emitted by group-chat orchestrations | `GroupChatRequestSentEvent` \| `GroupChatResponseReceivedEvent` | orchestration-internal | +| `"handoff_sent"` | emitted by handoff orchestrations | `HandoffSentEvent` | orchestration-internal | +| `"magentic_orchestrator"` | emitted by Magentic orchestrations | `MagenticOrchestratorEvent` | orchestration-internal | ### Factory methods (classmethod) @@ -80,7 +83,7 @@ WorkflowEvent( | `failed` | `(details: WorkflowErrorDetails, data=None) → WorkflowEvent[DataT]` | Run termination | | `warning` | `(message: str) → WorkflowEvent[str]` | User-emitted diagnostic | | `error` | `(exception: Exception) → WorkflowEvent[Exception]` | User-emitted diagnostic | -| `emit` | `(data: DataT) → WorkflowEvent[DataT]` *(deprecated)* | Produces `"data"` events; prefer `ctx.yield_output()` | +| `emit` | `(executor_id: str, data: DataT) → WorkflowEvent[DataT]` *(deprecated, emits `DeprecationWarning`)* | Produces `"data"` events; prefer `ctx.yield_output()` | | `request_info` | `(request_id, source_executor_id, request_data, response_type) → WorkflowEvent[DataT]` | Human-in-the-loop pause | | `superstep_started` | `(iteration: int, data=None) → WorkflowEvent[DataT]` | Pregel superstep begin | | `superstep_completed` | `(iteration: int, data=None) → WorkflowEvent[DataT]` | Pregel superstep end | @@ -234,7 +237,7 @@ AgentContext( | `messages` | `list[Message]` | Messages sent to the agent. Mutate to inject/remove messages before the call. | | `session` | `AgentSession \| None` | The current session, or `None` for stateless runs. | | `tools` | tool types | Run-level tool overrides. `None` → agent's declared tools apply. | -| `options` | `dict[str, Any]` | Merged run options (model, temperature, etc.). | +| `options` | `Mapping[str, Any] \| None` | Merged run options (model, temperature, etc.). May be `None` when no options were supplied — guard before indexing. | | `stream` | `bool` | `True` for streaming invocations. | | `compaction_strategy` | `CompactionStrategy \| None` | Per-run compaction override. | | `tokenizer` | `TokenizerProtocol \| None` | Per-run tokenizer override. | @@ -303,7 +306,7 @@ class DateInjectorMiddleware(AgentMiddleware): import datetime today = datetime.date.today().isoformat() context.messages = [ - Message.from_system(f"Today's date is {today}."), + Message("system", [f"Today's date is {today}."]), *context.messages, ] await call_next() @@ -324,7 +327,7 @@ class MockMiddleware(AgentMiddleware): async def process(self, context: AgentContext, call_next): # Skip call_next entirely — return canned response context.result = AgentResponse( - messages=[Message.from_assistant(self._text)], + messages=[Message("assistant", [self._text])], ) ``` @@ -334,7 +337,7 @@ class MockMiddleware(AgentMiddleware): **Module:** `agent_framework._middleware` (re-exported via `agent_framework`) -> **Experimental:** requires `ExperimentalFeature.AGENT_HOOKS` to be acknowledged. +> **Experimental** (`ExperimentalFeature.AGENT_HOOKS`): there is no opt-in call. The first use of an API in this feature emits a one-time `ExperimentalWarning` (a `FutureWarning` subclass). To silence it: `from agent_framework._feature_stage import ExperimentalWarning` then `warnings.filterwarnings("ignore", category=ExperimentalWarning)`. A `MiddlewareBundle` groups several middleware objects into one opaque, indivisible unit. Features like `create_agent_hooks_middleware()` return a bundle because their internal middleware objects only uphold their contract when installed together — a bundle prevents accidental partial installation. @@ -401,23 +404,23 @@ agent = Agent( ### Example — bundle returned by a factory (agent-hooks pattern) ```python -from agent_framework import Agent, acknowledge_experimental_feature, ExperimentalFeature -from agent_framework import create_agent_hooks_middleware +from typing import Any + +from agent_framework import Agent, create_agent_hooks_middleware from agent_framework.openai import OpenAIChatClient # agent_hooks Interceptor objects come from the agent-hooks-sdk package. # Install it: pip install --pre agent-hooks-sdk -from agent_hooks import Interceptor, InterceptionContext, Verdict - -acknowledge_experimental_feature(ExperimentalFeature.AGENT_HOOKS) +from agent_hooks import AgentContext as HookContext, Interceptor, Verdict class LoggingInterceptor(Interceptor): """Log each interception point and allow all through.""" - def intercept(self, context: InterceptionContext) -> Verdict: - # InterceptionContext is a mapping; access data via .get() - print(f"[Hook] point={context.get('interception_point')!r} agent={context.get('agent_id')!r}") + def intercept(self, context: HookContext) -> Verdict: + # agent_hooks.AgentContext is a plain Mapping[str, Any]; read it by key. + point: Any = context.get("interception_point") + print(f"[Hook] point={point!r}") return Verdict.allow() @@ -438,7 +441,7 @@ agent = Agent( **Module:** `agent_framework._evaluation` (re-exported via `agent_framework`) -> **Experimental:** requires `ExperimentalFeature.EVALS` to be acknowledged. +> **Experimental** (`ExperimentalFeature.EVALS`): there is no opt-in call. The first use of an API in this feature emits a one-time `ExperimentalWarning` (a `FutureWarning` subclass). To silence it: `from agent_framework._feature_stage import ExperimentalWarning` then `warnings.filterwarnings("ignore", category=ExperimentalWarning)`. These two types work together in the evaluation harness. `ConversationSplitter` is a **structural protocol** — any callable with the signature `(list[Message]) → tuple[list[Message], list[Message]]` satisfies it. `ConversationSplit` is an **enum** of built-in splitters that also satisfy the protocol. @@ -454,6 +457,8 @@ Both members are callable: `query_msgs, response_msgs = ConversationSplit.LAST_T ### `ConversationSplitter` protocol ```python +from agent_framework import Message + # Any callable with this signature satisfies ConversationSplitter: def my_splitter( conversation: list[Message], @@ -466,15 +471,12 @@ def my_splitter( ```python import asyncio from agent_framework import ( - Agent, EvalItem, EvalCheck, CheckResult, LocalEvaluator, ConversationSplit, - Message, acknowledge_experimental_feature, ExperimentalFeature, + EvalItem, CheckResult, LocalEvaluator, ConversationSplit, Message, ) -from agent_framework.openai import OpenAIChatClient - -acknowledge_experimental_feature(ExperimentalFeature.EVALS) -# An EvalCheck is a callable: (EvalItem) -> CheckResult +# A check is any callable (EvalItem) -> CheckResult | Awaitable[CheckResult]. +# (The EvalCheck alias lives in agent_framework._evaluation; it is not re-exported.) # item.response is already a str (the joined assistant text from the response split). async def factual_check(item: EvalItem) -> CheckResult: """Pass if the response contains 'Paris'.""" @@ -539,10 +541,10 @@ def split_before_tool_call( from agent_framework import ConversationSplit, Message conversation = [ - Message.from_user("Plan a weekend trip to London."), - Message.from_assistant("Sure! Day 1: Arrive and check into your hotel..."), - Message.from_user("What about museums?"), - Message.from_assistant("London has the British Museum, the Tate Modern, and the Natural History Museum..."), + Message("user", ["Plan a weekend trip to London."]), + Message("assistant", ["Sure! Day 1: Arrive and check into your hotel..."]), + Message("user", ["What about museums?"]), + Message("assistant", ["London has the British Museum, the Tate Modern, and the Natural History Museum..."]), ] query, response = ConversationSplit.FULL(conversation) @@ -556,7 +558,7 @@ print("Response messages:", [m.role for m in response]) # ['assistant', 'user', **Module:** `agent_framework._vectors` (re-exported via `agent_framework`) -> **Experimental:** requires `ExperimentalFeature.VECTOR_STORES` to be acknowledged. +> **Experimental** (`ExperimentalFeature.VECTOR_STORES`): there is no opt-in call. The first use of an API in this feature emits a one-time `ExperimentalWarning` (a `FutureWarning` subclass). To silence it: `from agent_framework._feature_stage import ExperimentalWarning` then `warnings.filterwarnings("ignore", category=ExperimentalWarning)`. `VectorStoreHistoryProvider` stores full conversation history in a provider-owned vector collection. Unlike `VectorCollectionContextProvider` (which exposes a caller-owned data model), this provider owns the collection schema and translates `Message` objects into a fixed history schema with optional embedding support. @@ -619,16 +621,9 @@ VectorStoreHistoryProvider( ```python import asyncio -from agent_framework import ( - Agent, acknowledge_experimental_feature, ExperimentalFeature, -) -from agent_framework._vectors import VectorStoreHistoryProvider -from agent_framework.openai import OpenAIChatClient - # Use any supported vector store, e.g. InMemoryStore (already deep-dived in Vol. 4) -from agent_framework import InMemoryStore - -acknowledge_experimental_feature(ExperimentalFeature.VECTOR_STORES) +from agent_framework import Agent, InMemoryStore, VectorStoreHistoryProvider +from agent_framework.openai import OpenAIChatClient async def main(): @@ -661,14 +656,9 @@ asyncio.run(main()) ```python import asyncio -from agent_framework import ( - Agent, InMemoryStore, acknowledge_experimental_feature, ExperimentalFeature, -) -from agent_framework._vectors import VectorStoreHistoryProvider +from agent_framework import Agent, InMemoryStore, VectorStoreHistoryProvider from agent_framework.openai import OpenAIChatClient, OpenAIEmbeddingClient -acknowledge_experimental_feature(ExperimentalFeature.VECTOR_STORES) - async def main(): store = InMemoryStore() @@ -711,7 +701,7 @@ async def reset_user_history(history_provider, session_id: str): **Module:** `agent_framework._harness._memory` (re-exported via `agent_framework`) -> **Experimental:** requires `ExperimentalFeature.HARNESS` to be acknowledged. +> **Experimental** (`ExperimentalFeature.HARNESS`): there is no opt-in call. The first use of an API in this feature emits a one-time `ExperimentalWarning` (a `FutureWarning` subclass). To silence it: `from agent_framework._feature_stage import ExperimentalWarning` then `warnings.filterwarnings("ignore", category=ExperimentalWarning)`. `MemoryStore` is the **abstract base class** for all memory backing stores used by `MemoryContextProvider`. It manages topic-based long-term memory organised as a set of per-topic markdown files plus a `MEMORY.md` index and a transcript archive. @@ -767,14 +757,9 @@ MemoryFileStore( ```python import asyncio -from agent_framework import ( - Agent, MemoryContextProvider, acknowledge_experimental_feature, ExperimentalFeature, -) -from agent_framework._harness._memory import MemoryFileStore +from agent_framework import Agent, MemoryContextProvider, MemoryFileStore from agent_framework.openai import OpenAIChatClient -acknowledge_experimental_feature(ExperimentalFeature.HARNESS) - async def main(): store = MemoryFileStore( @@ -920,7 +905,7 @@ class InMemoryMemoryStore(MemoryStore): **Module:** `agent_framework._harness._memory` (re-exported via `agent_framework`) -> **Experimental:** requires `ExperimentalFeature.HARNESS`. +> **Experimental** (`ExperimentalFeature.HARNESS`): there is no opt-in call. The first use of an API in this feature emits a one-time `ExperimentalWarning` (a `FutureWarning` subclass). To silence it: `from agent_framework._feature_stage import ExperimentalWarning` then `warnings.filterwarnings("ignore", category=ExperimentalWarning)`. `MemoryTopicRecord` represents one **topic memory file** — the unit of long-term memory storage. Each record has a human-readable topic, a stable `slug` (filesystem name), a short `summary`, a deduplicated list of `memories` (bullet points), a timestamp, and the session IDs that contributed to this topic. @@ -1223,7 +1208,7 @@ FunctionInvocationContext( | `kwargs` | `dict[str, Any]` | Extra kwargs forwarded to the tool. | | `tools` | `list[ToolTypes] \| None` | **Live** mutable tool list for the current agent run. `None` outside a function-calling loop. | -### Methods (experimental: `ExperimentalFeature.PROGRESSIVE_TOOLS`) +### Methods (experimental: `ExperimentalFeature.PROGRESSIVE_TOOLS`, emits `ExperimentalWarning` on first use) ```python context.add_tools( @@ -1286,14 +1271,9 @@ class ToolInputPatternGuard(FunctionMiddleware): ```python import asyncio -from agent_framework import ( - Agent, FunctionInvocationContext, tool, - acknowledge_experimental_feature, ExperimentalFeature, -) +from agent_framework import Agent, FunctionInvocationContext, tool from agent_framework.openai import OpenAIChatClient -acknowledge_experimental_feature(ExperimentalFeature.PROGRESSIVE_TOOLS) - @tool def factorial(n: int) -> int: @@ -1509,15 +1489,16 @@ class ConfigurableAgent: ## What's new in 1.19.0 -The 1.19.0 release refines several of the APIs deep-dived in this and prior volumes. Key areas: +Comparing the public `agent_framework` exports of 1.18.0 and 1.19.0, the release adds the vector-store memory layer covered in section 5 of this volume. Nothing was removed. -| Area | Change | +| Addition | Notes | |---|---| -| **Progressive tools** | `FunctionInvocationContext.add_tools()` / `remove_tools()` stabilised under `ExperimentalFeature.PROGRESSIVE_TOOLS`. All-or-nothing batch semantics: a duplicate name raises before the live list is mutated. | -| **Vector history** | `VectorStoreHistoryProvider` adds `store_context_from` for fine-grained control over which source IDs have their context messages persisted. | -| **Memory harness** | `MemoryFileStore.search_transcripts` now resolves the target transcript file stem via `_transcript_file_stem()` — supporting even very long session IDs stored under an irreversible digest. | -| **WorkflowEvent** | `WorkflowEvent.executor_bypassed` documents the cache-hit replay path more precisely. The `emit()` factory deprecation warning is now emitted with `stacklevel=2` for correct source attribution. | -| **ChatOptions** | `conversation_id` field added for providers that support conversation-level threading. | +| `VectorStoreHistoryProvider` | New in 1.19.0 (see section 5). Experimental under `ExperimentalFeature.VECTOR_STORES`. | +| `VectorCollectionContextProvider` | New context provider that retrieves from a vector collection. Also experimental under `VECTOR_STORES`. | +| `create_get_tool`, `create_upsert_tool`, `create_delete_tool` | New factories that expose a vector collection to an agent as function tools. Experimental under `VECTOR_STORES`. | +| `ResponseInvalidatedException` | New `ChatClientException` subclass. | + +The other APIs in this volume (`WorkflowEvent`, the memory harness, `ChatOptions`, progressive tools) behave the same as in 1.18.0. They are documented here because earlier volumes didn't cover them, not because they changed. --- From 59d42f55f9e326801df8c95503a2df3110491753 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 13:15:05 +0000 Subject: [PATCH 22/22] fix(v5): evict stale topic record on slug reuse in write_topic() When a topic is renamed but retains its stable slug, write_topic() was inserting under the new topic key without removing the old record. list_topics() and rebuild_index() would then return both copies. Before inserting, check if the slug already maps to a different topic name and pop that stale record from _topics first, keeping _topics and _slug_idx in sync (matching file-backed store semantics). Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01J66Vy9idLUFLqJfgqkM5uq --- ...soft_agent_framework_python_class_deep_dives_v5.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md index 84836626..a54da409 100644 --- a/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md +++ b/src/content/docs/microsoft-agent-framework-guide/python/microsoft_agent_framework_python_class_deep_dives_v5.md @@ -862,8 +862,15 @@ class InMemoryMemoryStore(MemoryStore): def write_topic(self, session, record, *, source_id): key = self._key(session, source_id) - self._topics.setdefault(key, {})[record.topic] = record - self._slug_idx.setdefault(key, {})[record.slug] = record.topic + topics = self._topics.setdefault(key, {}) + slug_idx = self._slug_idx.setdefault(key, {}) + # If this slug already points to a different topic name, remove the old + # record so list_topics() never returns both the stale and renamed copies. + prior_topic = slug_idx.get(record.slug) + if prior_topic is not None and prior_topic != record.topic: + topics.pop(prior_topic, None) + topics[record.topic] = record + slug_idx[record.slug] = record.topic def delete_topic(self, session, *, source_id, topic): key = self._key(session, source_id)