Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@
}
]
}
},
{
"match": { "userMessage": "conference travel", "hasToolResult": true },
"response": {
"content": "Yes — submit the $900 conference travel expense. It is under the $1,500 travel cap; attach itemized receipts since it exceeds the $75 receipts threshold."
}
},
{
"match": { "systemMessage": "expense-policy researcher" },
"response": {
"content": "- Travel expenses up to $1,500 per trip are reimbursable with manager approval.\n- Any expense over the $75 receipts threshold requires itemized receipts.\n- Conference travel must be filed within 30 days of the trip end date."
}
},
{
"match": { "userMessage": "conference travel" },
"response": {
"toolCalls": [
{
"name": "research_policy",
"arguments": {
"category": "travel",
"amount": 900
}
}
]
}
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
import { test, expect } from '@playwright/test';
import { submitAndWaitForResponse } from '@threadplane-internal/e2e-harness';

// First proof outside unit tests that the neutral Agent contract's interrupt
// path works against a genuinely non-LangGraph AG-UI backend: the Microsoft
Expand Down Expand Up @@ -34,3 +35,22 @@ test.describe('cockpit runtimes/microsoft-agent-framework: expense approval', ()
await expect(page.getByText(/has been submitted for reimbursement/i)).toBeVisible({ timeout: 30_000 });
});
});

// Delegation over the same non-LangGraph bridge: the orchestrator's
// `research_policy` tool streams the tool-less `policy_researcher`
// specialist, and the queue-merge emitter (src/subagent_emitter.py)
// translates its deltas into SUBAGENT_STARTED / attributed TEXT_MESSAGE_* /
// SUBAGENT_FINISHED wire events. The @threadplane/ag-ui reducer keys the
// subagent to its spawning toolCallId, so <chat-tool-calls> renders the
// delegation inline as a <chat-subagent-card> instead of a tool-call chip.
test.describe('cockpit runtimes/microsoft-agent-framework: subagent delegation', () => {
test('rt-maf: delegated policy research renders a streaming subagent card', async ({ page }) => {
const bubble = await submitAndWaitForResponse(
page,
'Should I submit a $900 conference travel expense? Research the policy first',
);
await expect(page.locator('chat-subagent-card')).toHaveCount(1);
await expect(page.locator('chat-subagent-card')).toContainText('policy_researcher');
await expect(bubble).toContainText(/policy|expense/i);
});
});

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@ dependencies = [
"uvicorn[standard]>=0.29",
]

[dependency-groups]
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.23",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
68 changes: 65 additions & 3 deletions cockpit/runtimes/microsoft-agent-framework/python/src/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
protocol-standard ``RUN_FINISHED.outcome = {type: 'interrupt', ...}`` and
resumes from the client's top-level ``resume`` entries.

No subagents surface: the MAF bridge emits no per-subagent
ACTIVITY_SNAPSHOT/ACTIVITY_DELTA stream (measured red upstream).
- subagents: ``research_policy`` delegates to the tool-less
``policy_researcher`` specialist Agent and streams its deltas through the
``delegation_*`` helpers in src/subagent_emitter.py, which merge standard
``SUBAGENT_*`` + attributed child ``TEXT_MESSAGE_*`` events into the run
stream at the run-wrapper seam (the MAF bridge natively emits NOTHING for
nested-agent activity inside a tool — measured red upstream and in
docs/wire-capture-subagents.md).

Model client: Azure OpenAI is the DEFAULT path — when
``AZURE_OPENAI_ENDPOINT`` is set the client routes to Azure (key auth via
Expand All @@ -32,6 +37,8 @@
from agent_framework.openai import OpenAIChatCompletionClient
from pydantic import BaseModel, Field

from . import subagent_emitter

_POLICIES = {
"meals": {"limit_usd": 300, "receipt_required_over_usd": 25, "notes": "Team meals require attendee count in the memo."},
"travel": {"limit_usd": 1500, "receipt_required_over_usd": 0, "notes": "Book through the travel portal when possible."},
Expand Down Expand Up @@ -97,6 +104,10 @@ def submit_expense(expense: Expense) -> str:

_INSTRUCTIONS = """You are an expense approval copilot.

Before recommending whether to submit an expense, delegate the policy
research to the specialist by calling `research_policy` with the category
and amount.

When the user asks to file an expense:
1. FIRST call `lookup_expense_policy` with the expense category.
2. THEN call `submit_expense` with the complete structured expense
Expand Down Expand Up @@ -138,12 +149,63 @@ def build_chat_client() -> OpenAIChatCompletionClient:
)


policy_researcher = Agent(
name="policy_researcher",
instructions=(
"You are an expense-policy researcher. Given an expense category and "
"amount, summarize the applicable policy rules in 3 short bullets."
),
client=build_chat_client(),
)


@tool(
name="research_policy",
description="Delegate policy research for this expense to a specialist.",
)
async def research_policy(category: str, amount: float) -> str:
"""Delegate policy research for this expense to a specialist.

Streams the ``policy_researcher`` specialist and mirrors each text delta
onto the AG-UI wire as attributed SUBAGENT_* / TEXT_MESSAGE_* events via
src/subagent_emitter.py (no-ops outside the wrapped run).

Args:
category: Expense category, e.g. 'meals' or 'travel'.
amount: Expense amount in USD.

Returns:
The specialist's complete policy summary.
"""
# Deterministically recorded by the run wrapper's pump before this body
# runs (the bridge streams TOOL_CALL_START/ARGS/END first); None when
# invoked outside a wrapped run.
tid = subagent_emitter.current_tool_call_id("research_policy")
subagent_emitter.delegation_started(tid, policy_researcher.name)
parts: list[str] = []
try:
prompt = (
f"Expense category: {category}. Amount: ${amount:.2f}. "
"Summarize the applicable policy rules."
)
async for update in policy_researcher.run(prompt, stream=True):
text = update.text
if text:
parts.append(text)
subagent_emitter.delegation_delta(tid, text)
except Exception as exc:
subagent_emitter.delegation_error(tid, str(exc))
raise
subagent_emitter.delegation_finished(tid)
return "".join(parts)


agent = AgentFrameworkAgent(
agent=Agent(
name="expense_approval_copilot",
instructions=_INSTRUCTIONS,
client=build_chat_client(),
tools=[lookup_expense_policy, submit_expense],
tools=[lookup_expense_policy, research_policy, submit_expense],
),
name="ExpenseApprovalCopilot",
description="Files expense reports with policy lookup, shared state, and human approval.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint

from .agent import agent
from .subagent_emitter import wrap_agent_run

app = FastAPI(title="cockpit-runtimes-microsoft-agent-framework")
add_agent_framework_fastapi_endpoint(app, agent, path="/agent")
# The wrapper is the SUBAGENT_* injection seam: the endpoint consumes
# protocol_runner.run, and wrap_agent_run merges the delegation tool's
# enqueued child events into that stream (src/subagent_emitter.py).
wrapped_agent = wrap_agent_run(agent)
add_agent_framework_fastapi_endpoint(app, wrapped_agent, path="/agent")


@app.get("/ok")
Expand Down
Loading
Loading