Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,55 @@ func start

Your agent is now running at `http://localhost:7071/agents/main/` with a built-in chat UI, HTTP API (`/agents/main/chat`, `/agents/main/chatstream`), and MCP tool exposed through the Functions MCP endpoint (`/runtime/webhooks/mcp`).

## Hybrid Functions with agent input

Use `agent_input` when an existing Function should keep its trigger and deterministic logic while invoking a markdown-defined agent in process. The smart decorator must be innermost, immediately above the handler:

```python
import json

from agent_framework import Agent
from azurefunctions.extensions.http.fastapi import Request, Response

from azure_functions_agents import AiApp

app = AiApp()


@app.route(route="orders/{orderId}", methods=["POST"])
@app.agent_input(arg_name="order_agent", agent_name="order-fulfillment")
async def process_order(
req: Request,
order_agent: Agent,
) -> Response:
response = await order_agent.run(
json.dumps(
{
"order_id": req.path_params["orderId"],
"order": await req.json(),
"task": "validate",
}
)
)
return Response(content=response.text)
```

Existing `func.FunctionApp()` instances can use `agent_input(app, ...)` instead. `create_function_app()` now returns an enhanced `AiApp` or `DurableAiApp`, preserving its existing declarative routes and triggers while allowing hybrid handlers to be added.

`agent_name` is the source filename stem or normalized slug, not the front-matter display name. For bindings, the agent file requires only string `name` and `description` fields; its markdown body supplies instructions and follows the standard environment-substitution behavior, including `substitute_variables: false`. Other per-agent front-matter fields are ignored. Model, timeout, system tools, discovered tools, skills, and MCP servers come from app-level configuration.

Function and activity handlers using `agent_input` must be declared with `async def`. They receive a raw `agent_framework.Agent` that is built and entered for that Function invocation, then closed when the handler returns, raises, or is cancelled. The app caches only an immutable blueprint and reusable dependency descriptions, never the live Agent or its MCP tools. These handlers control sessions, run options, middleware, streaming, and model-call timeouts; the Azure Functions invocation timeout remains the outer bound. Do not retain the Agent after the handler returns.

Durable apps use `DurableAiApp` or a caller-owned `df.DFApp` with an explicit mode:

- `mode="activity"` injects a raw Agent into an `async def` activity handler.
- `mode="orchestrator"` injects `DurableAiAgent`; `run()` returns a replay-safe Durable task backed by the generated `_afa_agent_binding_run` activity. Streaming is unsupported.

See [`samples/hybrid-function-agent/`](samples/hybrid-function-agent/) for an `AiApp` with HTTP and queue handlers, and [`samples/hybrid-durable-agent/`](samples/hybrid-durable-agent/) for a `DurableAiApp` with activity and orchestrator handlers.
Both samples demonstrate a production-oriented handoff: deterministic Python validates
and normalizes orders, calculates trusted monetary fields, derives review signals, and
removes unnecessary PII before the agent performs contextual assessment or planning.

## Features

**Architecture overview:** see [`docs/architecture.md`](docs/architecture.md) for the module map and data flow pipeline.
Expand Down
23 changes: 20 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ flowchart LR
J["client_manager.py<br/>ClientManager"] -.->|"chat client"| K["runner.py<br/>run_agent<br/>run_agent_stream<br/>build_subagent_tools"]
H -.->|"handler closures + AgentCatalog"| K
K -.->|"prompt + tools + session"| L["Microsoft Agent Framework"]
A -->|"binding projection"| M["composition.py<br/>ProjectSnapshot"]
M -->|"BindingAgentEntry"| N["bindings.py<br/>agent_input / AiApp / DurableAiApp"]
N -.->|"cached AgentBlueprint"| O["hydration.py<br/>fresh Agent hydration"]
O -.->|"entered Agent per invocation"| L
```

Read left to right: files on disk become typed config, typed config becomes a
Expand All @@ -48,12 +52,16 @@ A few boundaries are worth calling out explicitly:
workflow runtime once, and registers agent surfaces (FRDs 0004 and 0007).
- **Registration is Azure-specific.** This is the first stage that knows about `azure.functions.FunctionApp`, decorators, routes, and trigger bindings.
- **Execution is deferred.** The runner is not part of startup registration; it is called later by handler closures when an HTTP route or trigger actually fires.
- **Smart bindings are a parallel projection.** `composition.py` reads only binding-required front matter and reuses the root-keyed discovery caches. It does not feed `AgentSpec` or weaken declarative validation.

## 3. Module map

| Package/module | Role | Key entry points |
| --- | --- | --- |
| `azure_functions_agents/app.py` | Top-level two-pass composition root. Before app mutation it builds the slug index, `AgentCatalog`, complete workflow-handler catalog, and immutable workflow-agent policy catalog. It chooses `DFApp` when any agent enables workflows, registers the workflow runtime once, then registers each agent. | `create_function_app()`, `_fail_on_duplicate_slugs()` |
| `azure_functions_agents/app.py` | Top-level two-pass composition root. Before app mutation it builds the slug index, `AgentCatalog`, complete workflow-handler catalog, and immutable workflow-agent policy catalog. It chooses `DurableAiApp` when any agent enables workflows (otherwise `AiApp`), registers the workflow runtime once, then registers each agent. | `create_function_app()`, `_fail_on_duplicate_slugs()` |
| `azure_functions_agents/composition.py` | Builds the immutable binding-only project snapshot and resolves a binding target by exact filename stem, then normalized slug. Requires only `name` and `description`, honors `substitute_variables` for markdown instructions, and discards all other per-agent front matter. | `load_project_snapshot()`, `compose_binding_target()` |
| `azure_functions_agents/bindings.py` | Owns the smart callable wrapper, enhanced app classes, per-app blueprint registry, raw Agent injection into async Functions and activities, and the replay-safe orchestrator proxy and generated Durable activity. It uses public SDK decorators and signatures without mutating `FunctionBuilder` internals. | `agent_input()`, `AiApp`, `DurableAiApp`, `DurableAiAgent` |
| `azure_functions_agents/hydration.py` | Owns immutable binding blueprints, fresh per-invocation MAF Agent construction and context management, and runtime-managed calls for the generated Durable activity. | `AgentBlueprint`, `open_agent()`, `run_blueprint()` |
| `azure_functions_agents/config/paths.py` | Resolves the app root and the optional config/history directory. | `set_app_root()`, `get_app_root()`, `resolve_config_dir()` |
| `azure_functions_agents/config/env.py` | Performs env-var substitution and bool coercion across config string values in YAML, JSON, front matter, and markdown body content. | `substitute_env_vars_in_value()`, `resolve_env_vars_in_data()`, `substitute_env_vars_in_text()`, `_to_bool()` |
| `azure_functions_agents/config/schema.py` | Defines the Pydantic models for raw, global, and merged config, including independent object-only chat and workflow Sub Agent grants. | `AgentSpec`, `GlobalConfig`, `ResolvedAgent`, `TriggerSpec`, `BuiltinEndpointsConfig`, `SubagentRef`, `WorkflowConfig`, `WorkflowSubagentRef` |
Expand All @@ -63,7 +71,7 @@ A few boundaries are worth calling out explicitly:
| `azure_functions_agents/config/validation.py` | Post-merge sanity checks for resolved agents, including rejecting unknown/duplicate/self references in both independent Sub Agent grants against the app-wide slug index. | `validate_resolved_agent()`, `validate_subagent_references()`, `validate_workflow_subagent_references()` |
| `azure_functions_agents/discovery/skills.py` | Walks `skills/<name>/SKILL.md` files, validates frontmatter, and caches the name→directory map for MAF's `SkillsProvider`. | `discover_skills()`, `clear_skills_cache()` |
| `azure_functions_agents/discovery/tools.py` | Imports `tools/*.py`, finds normal `FunctionTool`/plain-function tools, discovers `@workflow_tool` Activity targets, and caches both inventories. | `discover_project_tools()`, `discover_user_tools()` |
| `azure_functions_agents/discovery/mcp.py` | Loads `mcp.json`, applies `resolve_env_vars_in_data()`, and translates remote HTTP server definitions into MAF MCP tool wrappers. | `discover_mcp_servers()` |
| `azure_functions_agents/discovery/mcp.py` | Loads `mcp.json`, applies `resolve_env_vars_in_data()`, caches immutable resolved server definitions, and constructs fresh MAF MCP wrappers for each owning Agent context. | `discover_mcp_server_definitions()`, `discover_mcp_servers()` |
| `azure_functions_agents/registration/capabilities.py` | Applies per-agent MCP/skills/tools filters and packages the final runtime inventory; also fails fast when an auto-derived `delegate_<slug>` tool name collides with another tool already on the same agent. | `AgentCapabilities`, `build_capabilities()`, `validate_subagent_tool_names()` |
| `azure_functions_agents/registration/catalog.py` | Freezes every agent's `ResolvedAgent` + `AgentCapabilities` into one immutable, slug-keyed `AgentCatalog`, built once at startup and threaded read-only into request handlers (FRD 0007). | `AgentCatalog`, `CatalogEntry`, `build_catalog()` |
| `azure_functions_agents/registration/_naming.py` | Fails fast via `allocate_unique_function_name()` / `allocate_unique_builtin_slug()` when two `.agent.md` files sanitize to the same identity slug — a **breaking change** (FRD 0007 §5 Decision #17) replacing the previous silent auto-suffix behavior; re-exports the `_slug.py` helpers for backward compatibility. | `allocate_unique_function_name()`, `allocate_unique_builtin_slug()` |
Expand Down Expand Up @@ -91,6 +99,7 @@ A few boundaries are worth calling out explicitly:
- `discovery/` answers **"what is available in this project folder?"**
- `app.py`'s composition root answers **"is this configuration internally consistent app-wide?"** (unique slugs, valid `subagents:` references) — the one cross-agent question no single `ResolvedAgent` can answer by itself.
- `registration/` answers **"which Azure Functions surfaces should exist for this agent?"**
- `composition.py` and `bindings.py` answer **"which markdown agent should this customer-owned handler receive?"**
- `system_tools/` answers **"which runtime-provided tools can be attached on demand?"**
- `runner.py` and `client_manager.py` answer **"once invoked, how does an agent call the model and its tools — including any specialist it delegates to?"**
- `_observability.py` (cross-cutting) answers **"what did the run do, and is a failure the app's, runtime's, platform's, or a delegated specialist's fault?"**
Expand Down Expand Up @@ -124,6 +133,14 @@ matter; it trusts typed resolved values and immutable catalogs. Steps 6-10 are
pass 1 and side-effect-free. Steps 11-12 are pass 2 and own all Azure Functions
mutation.

### Smart binding startup and execution

`AiApp.agent_input()` and the free `agent_input(app, ...)` decorator use a separate binding-only path. At import time, the innermost decorator loads a per-app `ProjectSnapshot`, checks binding identity, resolves the requested source stem or slug, removes the injected parameter from the worker-facing signature, and returns a normal callable for the outer Azure decorator. Existing declarative parsing remains unchanged.

Function and activity handlers using `agent_input` must be coroutines. At invocation, each receives a raw `agent_framework.Agent` built from the app-owned immutable `AgentBlueprint`. The wrapper enters the Agent on the worker's current event loop and always closes it after the handler; the customer controls sessions, options, middleware, streaming, number of calls, and model-call timeout. Fresh chat clients, history providers, web/sandbox tools, MCP wrappers, mutable tool lists, and Agent contexts prevent state or lifecycle sharing across invocations of the same slug.

For Durable orchestrators, `DurableAiAgent.run()` performs no model or tool I/O. It validates a JSON payload and calls the generated `_afa_agent_binding_run` activity. Replay schedules the same history action and receives its recorded JSON result. The generated activity hydrates and closes a fresh Agent around one runtime-managed call. Durable Entity injection is not supported.

## 4. Pipeline stages

The `create_function_app()` docstring in `src/azure_functions_agents/app.py:create_function_app()` is the source of truth. The steps below restate it in module terms.
Expand Down Expand Up @@ -408,7 +425,7 @@ This design keeps global config declarative: shared config says what exists, whi

### Other notable boundaries

- **Skills:** discovered as `SKILL.md` directories and handed to MAF's `SkillsProvider`. The provider exposes `load_skill` / `read_skill_resource` tools to the agent and scopes file access to the skill directory by design — no runtime-wide file tools required.
- **Skills:** discovered as `SKILL.md` directories and handed to MAF's `SkillsProvider`. The provider exposes `load_skill` / `read_skill_resource` tools to the agent and scopes file access to the skill directory by design — no runtime-wide file tools required. Because Functions execute unattended and repository-local skills are trusted application inputs, those read-only operations do not require approval; `run_skill_script` remains approval-gated and has no runner by default.
- **Connectors:** connector actions are exposed to agents through MCP servers in `mcp.json`; connector-triggered agents use `trigger.type: connector_trigger`.
- **Built-in endpoints:** endpoint registration is a separate module so the trigger-registration path stays focused on Azure Function bindings rather than UI and chat surface concerns.
- **Multi-agent delegation:** `subagents:` is itself an extension point of sorts — it lets an agent's own front matter opt other, already-registered agents into its tool set without any code changes. See Section 5.
Expand Down
Loading