Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7260d2d
Add pluggable Agent extension packages
hallvictoria Sep 2, 2026
e43d664
rename
hallvictoria Sep 2, 2026
b08eee2
remove top-level import
hallvictoria Sep 2, 2026
31e55c5
Address feedback
hallvictoria Sep 3, 2026
55c9146
Add validation for client_factory option
hallvictoria Sep 3, 2026
9eba138
per agent provider
hallvictoria Sep 3, 2026
55c3a0a
Merge branch 'hallvictoria/pluggable-agent-extensions' of https://git…
hallvictoria Sep 3, 2026
10932d0
v1 for durable
hallvictoria Sep 3, 2026
336476c
Update file reading to use context manager
hallvictoria Sep 3, 2026
13ff502
feedback
hallvictoria Sep 3, 2026
8912f03
clean
hallvictoria Sep 3, 2026
a9a051c
skill + mcp support
hallvictoria Sep 3, 2026
d2c4d04
simplify
hallvictoria Sep 3, 2026
5103b0c
feedback
hallvictoria Sep 3, 2026
e954fad
fix sample
hallvictoria Sep 3, 2026
a48f639
feedback
hallvictoria Sep 8, 2026
82d0fe8
Rename to agents-extension
hallvictoria Sep 8, 2026
a1e876b
update to durable v2.x support
hallvictoria Sep 8, 2026
db1a371
fix directory structure, rename to AgentFunctionApp, remove Durable s…
hallvictoria Sep 9, 2026
7e3853a
update docs
hallvictoria Sep 9, 2026
db25265
improve type checking
hallvictoria Sep 9, 2026
0901b1a
fix tests
hallvictoria Sep 9, 2026
55da60a
feedback
hallvictoria Sep 10, 2026
2777aa3
improve samples
hallvictoria Sep 10, 2026
edb9d0a
durable context typing
hallvictoria Sep 10, 2026
764b0bd
Prototype lazy-owned DAFX support for agent bindings
ahmedmuhsin Sep 9, 2026
923e77f
Host durable markdown agents through discovery and bindings
ahmedmuhsin Sep 9, 2026
164bd0e
Discover YAML workflows and host them through DAFX
ahmedmuhsin Sep 10, 2026
7ef1146
Delegate YAML workflows to configurable native MAF factories
ahmedmuhsin Sep 10, 2026
9bd9937
Separate discovery and HTTP exposure and add durable workflow bindings
ahmedmuhsin Sep 10, 2026
2695a46
Record final rebase validation
ahmedmuhsin Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ A supported Python version is required - see

## Available extensions
* [Base extension](azurefunctions-extensions-base/README.md)
* [Agent provider base](azurefunctions-agents-extensions-base/README.md)
* [Microsoft Agent Framework](azurefunctions-agents-extensions-agent-framework/README.md)
* [Azure Blob Storage bindings](azurefunctions-extensions-bindings-blob/README.md)
* [Azure Cosmos DB bindings](azurefunctions-extensions-bindings-cosmosdb/README.md)
* [Azure Event Hubs bindings](azurefunctions-extensions-bindings-eventhub/README.md)
Expand Down
21 changes: 21 additions & 0 deletions azurefunctions-agents-extensions-agent-framework/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
recursive-include azurefunctions *.py *.pyi
recursive-include tests *.py
include LICENSE README.md
335 changes: 335 additions & 0 deletions azurefunctions-agents-extensions-agent-framework/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
# Azure Functions Microsoft Agent Framework Extension

Inject Microsoft Agent Framework Agents built from raw `.agent.md` instructions
into Python Azure Functions.

## Install

```text
pip install azurefunctions-agents-extensions-agent-framework
```

Install Durable Functions support with the durable extra:

```text
pip install "azurefunctions-agents-extensions-agent-framework[durable]"
```


Install remote MCP transport and Entra support with the MCP extra:

```text
pip install "azurefunctions-agents-extensions-agent-framework[mcp]"
```

## Use an Agent app

Create a zero-argument factory that returns a fresh MAF chat client. A new
client and Agent context are created and closed for every Function invocation.

```python
import azure.functions as func
from agent_framework import Agent
from azurefunctions.agents.extensions.agent_framework import AgentFunctionApp


def create_chat_client():
from agent_framework.openai import OpenAIChatClient

return OpenAIChatClient()


app = AgentFunctionApp(client_factory=create_chat_client)


@app.route(route="orders", methods=["POST"])
@app.markdown_agent(arg_name="agent", agent_name="orders")
async def process_order(req: func.HttpRequest, agent: Agent):
response = await agent.run(req.get_body().decode())
return response.text
```

`AgentFunctionApp` is owned by this extension and subclasses
`azure.functions.FunctionApp`. The Azure Functions SDK does not need Agent APIs
or modifications. One app uses the Microsoft Agent Framework provider selected
by this package.

Place the complete instructions at `orders.agent.md` or
`agents/orders.agent.md`. The file is raw UTF-8 text; no front matter or runtime
configuration is interpreted.

## Skills and MCP servers

Skills and MCP servers are discovered automatically from the app root and are
available to each Agent binding by default:

```text
skills/inventory/SKILL.md
mcp.json
```

`SKILL.md` uses Agent Skills frontmatter:

```markdown
---
name: inventory
description: Look up inventory policy and warehouse constraints.
---

Use the references in this skill when assessing stock.
```

The base extension discovers Skill directory paths without reading their
contents. Microsoft Agent Framework parses and validates each `SKILL.md` when
it loads the file-based Skills provider.

V1 MCP discovery supports remote HTTP transports only:

```json
{
"servers": {
"inventory": {
"type": "streamable-http",
"url": "$INVENTORY_MCP_URL",
"tools": ["lookup_stock", "reserve_stock"],
"headers": {"X-Tenant": "%TENANT_ID%"},
"auth": {
"scope": "$INVENTORY_MCP_SCOPE",
"client_id": "%AZURE_CLIENT_ID%"
}
}
}
}
```

`$VAR` and `%VAR%` references are resolved for each invocation, not during
discovery. Missing values fail before connecting. Servers configured with
headers or Entra authentication must use HTTPS; HTTP is accepted only for
loopback development. Exposed MCP tool names are prefixed with the server name
to prevent collisions between servers. Credentials, tokens, HTTP clients, MCP
tools, and Agents are fresh invocation-owned resources and are closed on
success, error, or cancellation. Do not place secrets directly in
source-controlled `mcp.json`; use environment references.

Every Agent in the Function App receives all valid Skills and MCP servers
discovered from the app root:

```python
from azurefunctions.agents.extensions.agent_framework import AgentFunctionApp

app = AgentFunctionApp(client_factory=create_chat_client)


@app.markdown_agent(arg_name="agent", agent_name="orders")
async def process_order(agent: Agent):
...
```

V1 has no app-level or per-binding capability selectors. Skill scripts and MCP
tools can perform privileged operations, so placing a definition under the app
root grants every Agent in that app access to it. Use separate Function Apps
when capabilities require isolation. Python `tools=` remain explicit because
they are supplied directly to the Microsoft Agent Framework Agent.

The normal markdown binding accepts `client_factory` and explicit Python
`tools` overrides. The extension owns the Agent client, name, instructions, and
discovered Skills/MCP integration. Configure `app_root` only when constructing
`AgentFunctionApp`; decorators do not override it.

## Durable Agents

Durable support is optional. This prototype pins both DAFX packages to
[DAFX PR #72](https://github.com/microsoft/agent-framework-durable-extension/pull/72)
at `aa9529ec489e16ac64b73bd68d5adbb8e4945258` for SDK 2 compatibility.
These Git dependencies are for local prototyping, not a PyPI release.

```text
pip install "azurefunctions-agents-extensions-agent-framework[durable]"
```

Set `discover_agents=True` to discover every `.agent.md` file directly in the app root
or its `agents/` directory. Each discovered agent gets a DAFX entity and an
automatic `POST /api/agents/{name}/run` endpoint with the default HTTP route
prefix. No handwritten HTTP function or orchestrator is required.

This explicitly publishes every discovered definition. Durable names must start
with an ASCII letter or digit and contain only ASCII letters, digits, hyphens, and
underscores. Ambiguous definitions and generated function-name collisions fail
rather than silently selecting an agent.

```python
app = AgentFunctionApp(client_factory=create_chat_client, discover_agents=True)
```

For orchestration, place `durable_markdown_agent` below `orchestration_trigger`
on a synchronous generator. The binding registers the selected markdown agent
without bulk discovery. It is private by default (`expose_http_endpoint=False`).
The injected object is a DAFX proxy, not a live Agent. Yield its tasks and share
a session across turns.

```python
app = AgentFunctionApp(client_factory=create_chat_client)


@app.orchestration_trigger(context_name="context")
@app.durable_markdown_agent(
arg_name="agent", agent_name="orders", context_name="context"
)
def orders(context, agent):
session = agent.create_session()
assessment = yield agent.run("Assess the order.", session=session)
plan = yield agent.run("Make a fulfillment plan.", session=session)
return {"assessment": assessment.text, "plan": plan.text}
```

Registration compiles recipes without constructing clients. At entity execution,
the lifecycle adapter enters the compiled binding's `open_agent()` context and
closes it after the run. Each execution creates fresh clients and tools; DAFX
restores conversation history from durable session state. Orchestrators keep the
native SDK context. The old `context.call_agent()` activity path is replaced by
the injected proxy.

Normal `markdown_agent()` remains invocation-scoped and unchanged. Durable
declarations collect registrations first. The inner DAFX host is constructed
at `get_functions()` only when an agent or workflow is registered. The outer
app indexes both registries, including the SDK's `BuiltIn__HttpActivity` and
`BuiltIn__HttpPollOrchestrator`. Health and MCP endpoints are disabled.

### Discovery, exposure, and policy

`discover_agents=False` and `discover_workflows=False` are independent defaults.
The constructor's `expose_agent_endpoints=True` and
`expose_workflow_endpoints=True` apply only to their respective bulk discovery
paths. Set either exposure option to `False` to register those definitions
without publishing their standalone HTTP endpoints.

Selective `durable_markdown_agent` and `durable_workflow` bindings instead use
their own `expose_http_endpoint=False` default. Set it to `True` on a binding
to publish that definition. Discovery and bindings share the same registry.
Repeated registration reuses the definition and combines exposure with logical
OR, so a private binding does not hide an endpoint already explicitly enabled.
Register all bindings before indexing.

Endpoint exposure is not application policy. A generated endpoint invokes its
agent or workflow directly and bypasses any validation in a handwritten parent
or starter. Private here means no standalone generated HTTP route, not a
separate authorization or execution boundary. Hosted HTTP endpoints require
the configured auth level, which defaults to a function key.

See the [endpoint-only local sample](samples/lazy-owned-dafx/README.md) and the
[durable binding sample](samples/durable-markdown-binding/README.md) for setup
and deterministic examples that do not need a model service.

## YAML workflows

YAML hosting is a separate opt-in. Install both optional extras. The workflows
extra uses `agent-framework-declarative>=1.0.3,<2`.

```text
pip install "azurefunctions-agents-extensions-agent-framework[durable,workflows]"
```

```python
app = AgentFunctionApp(
client_factory=create_chat_client,
discover_workflows=True,
)
```

Workflow discovery does not require agent discovery. Only `*.workflow.yaml` and
`*.workflow.yml` directly in the app root or its `workflows/` directory are
discovered, not arbitrary YAML or nested files. Discovered entry files must stay
within the app root. Each loaded result must be a MAF `Workflow` with a stable
name of 1–63 ASCII letters, digits, hyphens, or underscores, starting with a
letter. Names must be unique ignoring case. An explicit YAML `name` keeps routes
predictable, but naming and YAML parsing otherwise follow MAF.

The extension calls the public `create_workflow_from_yaml_path(path)` method and
supplies the graphs to DAFX's `workflows=` constructor. With the default route
prefix, each graph gets
`POST /api/workflow/NAME/run`, `GET /api/workflow/NAME/status/{instanceId}`, and
`POST /api/workflow/NAME/respond/{instanceId}/{requestId}`. No custom
orchestration or handwritten HTTP handlers are needed.

By default, `WorkflowFactory(agents=...)` receives `MarkdownDurableAgent`
adapters for **all** discovered Markdown agents, including agents selected by
dynamic names. This does not register standalone agent entities or HTTP routes
unless `discover_agents=True` or a selective agent binding also registers them.
To configure MAF directly, pass a configured `WorkflowFactory` object as
`workflow_factory=`. It is allowed without discovery, including with a selective
workflow binding. That object is used unchanged. Its agent registry is not
automatically merged with discovered
Markdown agents. Configure its `agent_factory`, agents, registered tools, HTTP
or MCP handlers, and configuration through MAF's public APIs.

The extension does not impose a separate YAML parser, action allowlist, or
restrictions on inline agents, file-based agents, dynamic agent references, or
workflow tool actions. These follow the installed MAF parser and builder,
including their warnings, errors, and required configuration. For example,
`InvokeFunctionTool` can use `WorkflowFactory.register_tool()`, while HTTP and
MCP actions need their MAF handlers. DAFX's hosting validations still apply.
This delegation is not a claim that every MAF feature has been execution-tested.

Relative file references inside YAML use native MAF resolution from the workflow
file's directory. They are not sandboxed by the entry-file containment check.
Treat workflow files and their references as trusted deployment content.

Agent actions execute as durable activities, not through the agent entity in the
same graph. Markdown adapters open and close fresh Agents, clients, and tools
per execution. Inline agents and agents supplied by a custom factory follow
MAF's or that factory's construction and resource lifecycle, which may construct
agents and clients during app initialization/indexing. The extension does not
wrap them in the Markdown lifecycle. Supplying a factory neither enables agent
discovery nor publishes standalone agent endpoints. `client_factory` remains
required even for a tool-only workflow. Such apps can pass a `NoReturn` sentinel
that raises if called, as the configured factory sample does.

### Bind a private child workflow

Use `durable_workflow` below `orchestration_trigger` to select a YAML workflow
without bulk discovery. The default `context_name` is `"context"`.

```python
app = AgentFunctionApp(client_factory=create_chat_client)


@app.orchestration_trigger(context_name="context")
@app.durable_workflow(arg_name="child", workflow_name="Child")
def parent(context, child):
outputs = yield child.run(context.get_input())
return {"child_outputs": outputs}
```

Without `workflow_file`, a new binding matches `Child.workflow.yaml` or
`Child.workflow.yml` directly in the app root or `workflows/`, before parsing.
Unrelated YAML definitions are not loaded. An explicit app-root-relative
`workflow_file` can select another filename within the app root. In either case,
the loaded workflow name must equal `workflow_name`. When reusing an already
registered graph, omit `workflow_file`.

`child.run(input_, instance_id=None)` returns a yieldable child-orchestration
task. It invokes `dafx-Child` through the native Durable context and returns
decoded workflow outputs, rather than calling `Workflow.run()` in-process.
Each invocation has its own workflow state. Binding alone creates no child run,
status, or response HTTP routes. Add `expose_http_endpoint=True` to the binding
only when standalone access is intended.

Forwarded input uses the same reserved-marker sanitization as DAFX's workflow
HTTP entry point. Workflow results are decoded only from the trusted child result.
The generic parent binding does not aggregate child human-input requests into a
parent workflow status endpoint. For child HITL, expose the child's management
routes or implement application management using the child instance ID.

Python support follows the installed MAF dependencies, not an extension-level
Python 3.14 rejection. Expression execution has been verified on Python 3.13.
MAF declarative 1.0.3 excludes its PowerFx dependency on Python 3.14, so those
expression checks remain on 3.13. Python 3.14 execution is not claimed as verified.

See the [local YAML sample](samples/durable-yaml-workflow/README.md) for shared
state, a Markdown agent call, and a separate question/response workflow. The
[configured factory sample](samples/configured-workflow-factory/README.md) uses
a registered function tool and configuration without an agent client. Neither
sample provisions a host or backend.
The [workflow binding sample](samples/durable-workflow-binding/README.md) calls a
private, agent-free YAML child from a parent generator and includes an HTTP
starter for the parent.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .apps import AgentFunctionApp
from .provider import AGENT_FRAMEWORK_PROVIDER_ID, ClientFactory

__all__ = [
"AGENT_FRAMEWORK_PROVIDER_ID",
"AgentFunctionApp",
"ClientFactory",
]

__version__ = '1.0.0b1'
Loading
Loading