feat: add agent binding - #159
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a Python “smart agent input binding” that lets existing Azure Functions (and Durable Functions) handlers inject a markdown-defined agent at runtime, while keeping triggers and deterministic business logic in customer code.
Changes:
- Introduces
agent_input(...)plusAiApp/DurableAiAppwrappers to inject a per-invocationagent_framework.Agent, and aDurableAiAgentproxy for replay-safe orchestrator usage. - Adds a binding-only composition + hydration pipeline (
composition.py→ cachedAgentBlueprint→ fresh agent per invocation), including MCP definition caching to rebuild fresh MCP tools per agent context. - Expands tests, docs, and adds hybrid samples demonstrating HTTP/Queue and Durable activity/orchestrator patterns.
Reviewed changes
Copilot reviewed 43 out of 43 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_registration_capabilities.py | Verifies unattended/read-only skill tools are non-approval-gated while scripts remain gated. |
| tests/test_package_imports.py | Updates public export surface assertions for new binding APIs. |
| tests/test_hydration.py | Adds unit coverage for blueprint hydration, agent context lifetime, concurrency, and managed timeouts. |
| tests/test_hybrid_binding_sample.py | Validates the new hybrid samples index correctly and behave as documented. |
| tests/test_discovery_mcp.py | Ensures MCP tool instances are rebuilt fresh even when definitions are cached. |
| tests/test_composition.py | Covers binding-only composition rules (minimal front matter, slug normalization, diagnostics). |
| tests/test_bindings.py | Exercises agent_input behavior across Function, Durable activity, and orchestrator modes. |
| src/azure_functions_agents/runner.py | Adjusts SkillsProvider wiring to allow unattended read-only operations. |
| src/azure_functions_agents/hydration.py | Implements immutable AgentBlueprint + per-invocation agent lifecycle and managed execution for Durable activity. |
| src/azure_functions_agents/discovery/mcp.py | Caches immutable MCP server definitions and rebuilds fresh MCP tools per owning agent context. |
| src/azure_functions_agents/composition.py | Adds binding-only project snapshot + target resolution without full declarative validation. |
| src/azure_functions_agents/bindings.py | Introduces the public binding API (agent_input, AiApp, DurableAiApp, DurableAiAgent) and generated internal Durable activity. |
| src/azure_functions_agents/app.py | Switches create_function_app() to return enhanced AiApp / DurableAiApp. |
| src/azure_functions_agents/init.py | Exports new binding APIs as part of the public package surface. |
| samples/README.md | Lists new hybrid samples and describes what they demonstrate. |
| samples/hybrid-function-agent/src/tools/order_totals.py | Sample tool used by the hybrid function agent. |
| samples/hybrid-function-agent/src/skills/order-review/SKILL.md | Sample skill used by the hybrid function agent. |
| samples/hybrid-function-agent/src/requirements.txt | Sample requirements entry for editable install. |
| samples/hybrid-function-agent/src/order-fulfillment.agent.md | Minimal binding-projected agent definition for sample. |
| samples/hybrid-function-agent/src/mcp.json | Sample MCP server config. |
| samples/hybrid-function-agent/src/local.settings.template.json | Sample local settings template. |
| samples/hybrid-function-agent/src/host.json | Sample host settings. |
| samples/hybrid-function-agent/src/function_app.py | Hybrid HTTP + (intended) queue trigger sample using AiApp.agent_input. |
| samples/hybrid-function-agent/src/agents.config.yaml | Sample global config for model/timeout. |
| samples/hybrid-function-agent/README.md | Sample README describing hybrid function binding usage. |
| samples/hybrid-durable-agent/src/tools/order_totals.py | Sample tool used by the hybrid durable agent. |
| samples/hybrid-durable-agent/src/skills/order-review/SKILL.md | Sample skill used by the hybrid durable agent. |
| samples/hybrid-durable-agent/src/requirements.txt | Sample requirements entry for editable install. |
| samples/hybrid-durable-agent/src/order-fulfillment.agent.md | Minimal binding-projected agent definition for durable sample. |
| samples/hybrid-durable-agent/src/mcp.json | Sample MCP server config. |
| samples/hybrid-durable-agent/src/local.settings.template.json | Sample local settings template. |
| samples/hybrid-durable-agent/src/host.json | Sample host settings. |
| samples/hybrid-durable-agent/src/function_app.py | Durable activity + orchestrator sample using DurableAiApp and DurableAiAgent. |
| samples/hybrid-durable-agent/src/agents.config.yaml | Sample global config for model/timeout. |
| samples/hybrid-durable-agent/README.md | Sample README describing hybrid durable binding usage. |
| README.md | Documents hybrid binding usage and constraints for Functions + Durable. |
| pyproject.toml | Updates agent-framework package pins to newer versions supporting the lifecycle/session patterns used. |
| docs/workflows.md | Documents how smart agent input relates to Durable workflows/orchestrators. |
| docs/observability.md | Documents new binding spans and emitted attributes/outcomes. |
| docs/front-matter-spec.md | Documents the reduced front-matter projection for agent_input bindings. |
| docs/frds/README.md | Adds FRD entry for agent input binding. |
| docs/frds/0008-agent-input-binding.md | Adds finalized FRD describing design, constraints, and test plan. |
| docs/architecture.md | Updates module map and pipeline stages to include binding projection + hydration. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/azure_functions_agents/bindings.py:100
- _BindingRuntime.resolve() caches blueprints under the raw
agent_namestring, butcompose_binding_target()internally usesagent_name.strip()for lookup. Passing a value with leading/trailing whitespace (e.g. from env/config) will bypass the cache fast-path and create duplicate dict entries for the same agent, causing unnecessary snapshot/definition work and unbounded key growth over time.
Normalize the key once (strip + non-empty check) and use that consistently for cache lookup and insertion before composing the target.
src/azure_functions_agents/composition.py:123
load_binding_definition()currently uses the raw markdown body (post.content) as instructions without applying the repo’s standard env-var substitution. In the declarative loader (config/loader.py), instructions are passed throughsubstitute_env_vars_in_text()by default, so binding-mode definitions that rely on$VARSin the markdown body will behave differently.
Consider applying the same substitution to post.content for binding projection so instruction handling is consistent across declarative and bound agents.
|
|
||
| class _BindingRuntime: | ||
| def __init__(self, app: func.FunctionApp, app_root: Path | None) -> None: | ||
| self.app = app |
There was a problem hiding this comment.
Although _RUNTIMES is a WeakKeyDictionary, each _BindingRuntime value holds a strong reference to the same app used as its key. This leaves a strong reference chain, _RUNTIMES -> runtime -> app, so dropping all external references to the app will not invalidate the weak key. The runtime—and the snapshots and blueprints it retains—therefore cannot be collected. Could _BindingRuntime store a weakref.ref(app) instead and dereference it only when registering the Durable activity?
There was a problem hiding this comment.
updated! _BindingRuntime now stores weakref.ref(app) and dereferences it only during Durable activity registration in bindings.py
…s-runtime into hallvictoria/agent-binding
| return BindingAgentDefinition( | ||
| name=name, | ||
| description=description, | ||
| instructions=str(post.content), |
There was a problem hiding this comment.
This binding projection uses the raw markdown body, bypassing the environment-variable substitution performed by the regular agent loader. As a result, instructions such as Contact $TEAM_NAME are resolved when the file is consumed by create_function_app(), but remain literal when the same file is consumed through agent_input. This conflicts with the documented authoring contract that $VAR/%VAR% placeholders in every *.agent.md markdown body are substituted by default. Even if substitute_variables is intentionally ignored by the minimal binding projection, could the binding path apply substitute_env_vars_in_text() unconditionally (or explicitly document and test this binding-specific exception)?
There was a problem hiding this comment.
updated - now it uses the canonical environment substitution behavior in composition.py, including honoring substitute_variables: false
|
You are supporting |
|
Thanks for the comment Tsuyoshi Ushio (@TsuyoshiUshio)! I'm not sure what you mean by supporting |
| stream, and Agent context entry and exit are asynchronous. MAF exposes no blocking | ||
| Agent execution or lifecycle protocol for a synchronous Function or activity handler. | ||
|
|
||
| Orchestrators remain synchronous generators and receive `DurableAiAgent`; `run()` |
There was a problem hiding this comment.
The DurableAiAgent proxy is a substantial second abstraction over the standard Durable activity pattern: it still generates an internal activity, but also owns the activity name, payload/result schema, retry/idempotency behavior, and what transcript data enters Durable history. That reduces the apparent call-site boilerplate without removing the underlying replay, serialization, and failure-semantics concepts. Could we keep agent_input focused on Functions and activities for v1, document that orchestrators must call an explicit activity, and defer this proxy until we have evidence that the standardized one-line form is needed? If we retain it, please add a clear rationale and define why the library—not the customer-owned activity—should own those Durable semantics.
| f"{source}: {reason}" | ||
| for source, reason in self._snapshot.discovery.failed_loads | ||
| ) | ||
| raise ValueError(f"Agent binding discovery failed: {failures}") |
There was a problem hiding this comment.
required: A malformed or unrelated tool, skill, or MCP asset causes _load_project_snapshot() to populate failed_loads, and this branch then rejects every agent binding. That means a valid bound agent cannot start merely because another discovered asset is broken, even when the binding does not use it. Could discovery failures be scoped to the selected agent or treated as non-fatal unless the referenced capability is required? At minimum, please document this app-wide failure coupling as an intentional contract.
| response_data = response.to_dict() if hasattr(response, "to_dict") else {} | ||
| result = { | ||
| "text": str(getattr(response, "text", "") or ""), | ||
| "messages": response_data.get("messages", []), |
There was a problem hiding this comment.
suggestion: Returning the full messages array (and usage details) from every agent run means the complete transcript is persisted in Durable orchestration history and replay payloads. For longer conversations this can grow history quickly and may retain sensitive content unnecessarily. Could the default result be reduced to the final text plus identifiers/usage, with full messages opt-in or summarized before crossing the Durable boundary? Please also make the history-size and transcript-retention tradeoff explicit in the public docs.
| ] | ||
| assert len(activity_bindings) == 1 | ||
| assert activity_bindings[0]["activity"] == "_afa_agent_binding_run" | ||
| internal_activity = next( |
There was a problem hiding this comment.
required: This test verifies that the generated activity is registered and has the expected type hint, but it never invokes _afa_agent_binding_run. Could we add a direct execution test that patches run_blueprint and asserts the payload is mapped into the invocation, the response is projected into the documented JSON shape, and non-serializable results fail explicitly? This is the runtime path that actually performs model work, so registration-only coverage could miss regressions in the generated activity.
| metadata: dict[str, object] = dict(post.metadata or {}) | ||
| name = _required_string(metadata, "name", resolved_source) | ||
| description = _required_string(metadata, "description", resolved_source) | ||
| substitute_variables = _to_bool(metadata.get("substitute_variables", True), default=True) |
There was a problem hiding this comment.
suggestion: The binding path substitutes variables in the markdown body, but it does not apply the existing substitution behavior to the required front-matter values before deriving the binding definition and slug. That can make the same agent file resolve differently between declarative loading and agent_input. Could we reuse the loader complete substitution path, or explicitly document that only instructions are substituted and front-matter values are intentionally excluded?
When we use orchestrator mode, DurableAiAgent instance (planner) use yield planner.run()
sometthing like: again, it is not mandatory. Just an idea. :) |

Purpose
Fixes https://github.com/Azure/azure-functions-bucees-planning/issues/1287
AiApp.agent_input()for injecting freshly hydrated MAF Agents into existing async Azure FunctionsDurableAiAppsupport for activities and replay-safe orchestrator proxiesAgentblueprints while creating and closing a newAgentper invocationExample customer experience:
Does this introduce a breaking change?
Pull Request Type
What kind of change does this Pull Request introduce?
How to Test
What to Check
Verify that the following are valid
Other Information