• Microsoft Agent Framework 1.0, part 2: a Jenkins-gated debate between agents

    This is Part 2 of the Microsoft Agent Framework 1.0 series. Part 1 went from hello-world to local tools to a remote MCP server. The natural next step is the one the docs only hint at: multiple agents talking to each other, with a third agent breaking the tie. A code-review gate is a good shape for this because the cost of a wrong decision is concrete (a broken merge or a blocked PR), and the inputs (a diff, a PR body, a scan report) are easy to feed in.

    I built one. It lives in a small repo called ai-gate. Jenkins runs it as a per-PR Kubernetes Job: the Job opens a debate between a velocity advocate and a caution advocate, a judge issues a verdict in a strict format, the result is posted back to the PR as a comment, and the Job exits 0, 1, or 2. Jenkins gates the deploy stage on that exit code. Everything below is from the real source.

    You need everything from Part 1 plus three deployments on the same Azure OpenAI account. I used gpt-4o-mini for the velocity advocate (cheap, fast), gpt-4o for the caution advocate (better long-form reasoning), and o4-mini for the judge (a reasoning model). The Job also needs a GitHub fine-grained PAT with contents:read and pull_requests:write on the repos you want to gate.

    Each persona is a system prompt. Velocity and caution are nearly mirror images. The judge fixes the output shape so a parser can read the verdict deterministically:

    VELOCITY = """
    SHIP-IT SAM, the velocity
    advocate. Argue the PR
    SHOULD merge.
    
    Rules:
    - Cite evidence from the diff,
    PR body, tests, scans.
    - 4-6 tight sentences.
    - Concede ONLY when CAUTION
    invalidates your case.
    """
    
    CAUTION = """
    HOLD-THE-LINE HOLLY, the
    quality advocate. Argue the
    PR should be BLOCKED.
    
    Rules:
    - Cite evidence from the diff,
    PR body, tests, scans.
    - 4-6 sentences. Be honest.
    - Never manufacture concerns.
    """
    
    JUDGE = """
    JUDGE-CARLA. Read the debate.
    Output STRICTLY:
    
    VERDICT: APPROVE|BLOCK|NEEDS_HUMAN
    CONFIDENCE: 0-100
    SUMMARY:
    
    KEY EVIDENCE FOR APPROVE:
    - <one line>
    
    KEY EVIDENCE FOR BLOCK:
    
    - <one line>
    
    NEXT ACTION:
    <one line>
    
    """
    
    

    Three rounds of velocity-then-caution, each agent seeing the rolling transcript. After the last round the judge gets the full transcript and emits a verdict in the strict format. One round is partisan; two leaves the last rebuttal unanswered; three forces both sides to confront their weakest point, then either concede or double down:

    def make(deploy, instr):
    return ChatClientAgent(
    chat_client=OAI(
    endpoint=ENDPOINT,
    api_key=KEY,
    deployment_name=deploy,
    api_version=VERSION,
    ),
    instructions=instr,
    )
    
    def run_debate(brief):
    v = make(VEL_DEP, VELOCITY)
    c = make(CAU_DEP, CAUTION)
    j = make(JUDGE_DEP, JUDGE)
    
    log = []
    roll = brief + "\n(no turns)\n"
    
    for n in range(1, ROUNDS + 1):
    out = v.run(
    roll +
    f"\nRound {n}. APPROVE."
    ).content
    log.append(("vel", out))
    
    out2 = c.run(
    roll +
    f"\nVEL: {out}\nBLOCK."
    ).content
    log.append(("cau", out2))
    
    roll += (
    f"\n--- round {n} ---"
    f"\nVEL: {out}"
    f"\nCAU: {out2}\n"
    )
    
    j_in = brief + (
    "\nTRANSCRIPT:\n" +
    "\n".join(
    f"### {w}\n{m}"
    for w, m in log
    ) +
    "\nIssue verdict."
    )
    v_out = j.run(j_in).content
    log.append(("judge", v_out))
    return log, v_out
    

    The strict judge format makes parsing a one-liner per field, not an LLM call. Map VERDICT to an exit code: APPROVE=0, BLOCK=1, NEEDS_HUMAN=2. Jenkins gates the deploy stage on that exit code, so APPROVE flows through and anything else stops the build. The whole thing wraps in one Jenkins shared-library function:

    def call(args) {
    def repo = args.repo
    def pr = args.pr
    def base = args.baseSha
    def head = args.headSha
    def slug = repo
    .replaceAll('[/_.]', '-')
    .toLowerCase()
    def sha = head.take(8)
    def name = (
    "ai-gate-${slug}" +
    "-${pr}-${sha}"
    )
    
    sh """
    envsubst  | kubectl apply -f -
    kubectl wait \\
    --for=condition=complete \\
    --timeout=600s \\
    -n ai-gate \\
    job/${name}
    kubectl logs -n ai-gate \\
    job/${name}
    POD=\$(kubectl get pod \\
    -n ai-gate \\
    -l job-name=${name} \\
    -o jsonpath='{.items[0]\\
    .metadata.name}')
    exit \$(kubectl get pod \\
    -n ai-gate \$POD \\
    -o jsonpath='{.status\\
    .containerStatuses[0].state\\
    .terminated.exitCode}')
    """
    }
    

    Drop that in vars/aiCodeGate.groovy in your Jenkins shared library, then any Jenkinsfile can add a stage that calls aiCodeGate(repo: ..., pr: env.CHANGE_ID, baseSha: env.CHANGE_TARGET_SHA, headSha: env.GIT_COMMIT) before its deploy stage.

    Per PR works out to roughly $0.05 to $0.20 depending on diff size, with 3 rounds of two agents plus one judge call over a ~15k-token context. Wall time is 30-90 seconds. The Foundry Hub and Project are deployed, so the next obvious step is swapping the direct Azure OpenAI chat client for a Foundry connection – that’s Part 3.


  • Microsoft Agent Framework 1.0: three runnable Python examples

    Microsoft Agent Framework reached 1.0 GA on May 21, 2026 for both .NET and Python. It consolidates the previous Semantic Kernel and AutoGen SDKs into one supported codebase with a stable API surface and a long-term support commitment.

    To see what zero-to-agent actually looks like against my own Azure OpenAI deployment, I wrote three small Python examples that progressively add capability: a hello-world agent, an agent with local Python tools, and an agent that connects to a remote MCP server. All three are real files I ran.

    You need Python 3.10 or newer, an Azure OpenAI deployment (I used gpt-4o-mini, which is gpt-4.1-mini behind the alias), the endpoint URL and an API key, and pip install agent-framework==1.0 python-dotenv. Drop the credentials in a .env:

    AOAI_ENDPOINT=https://...
    AOAI_DEPLOYMENT=gpt-4o-mini
    AOAI_API_KEY=...
    AOAI_API_VERSION=2024-10-21
    

    The first example is a single agent that takes one prompt and returns one response. No tools, no state, no MCP. If it returns a sentence, the SDK and the deployment are both working:

    import os
    from dotenv import load_dotenv
    from agent_framework import (
        ChatClientAgent,
    )
    from agent_framework.azure import (
        AzureOpenAIChatClient,
    )
    
    load_dotenv()
    E = os.environ
    
    client = AzureOpenAIChatClient(
        endpoint=E["AOAI_ENDPOINT"],
        api_key=E["AOAI_API_KEY"],
        deployment_name=E["AOAI_DEPLOYMENT"],
        api_version=E["AOAI_API_VERSION"],
    )
    
    agent = ChatClientAgent(
        chat_client=client,
        instructions=(
            "Terse release-notes "
            "summarizer. One sentence."
        ),
    )
    
    prompt = (
        "Summarize: Azure VPN P2S "
        "now supports user-group "
        "IP pools, GA 2026-05-21."
    )
    print(agent.run(prompt).content)
    

    Adding a tool is one more parameter. The Annotated[..., "description"] pattern feeds each parameter description into the tool schema the model sees; the function docstring becomes the tool description:

    from typing import Annotated
    
    PRICES = {
        "MSFT": 412.10,
        "TSLA": 178.42,
        "PGEN": 1.23,
    }
    
    def get_price(
        sym: Annotated[
            str, "Ticker, uppercase"
        ],
    ) -> str:
        "Mock price for a ticker."
        p = PRICES.get(sym.upper())
        if p is None:
            return f"unknown {sym}"
        return f"{sym}: ${p:.2f}"
    
    agent = ChatClientAgent(
        chat_client=client,
        instructions=(
            "Portfolio assistant. "
            "Use the tool for prices."
        ),
        tools=[get_price],
    )
    
    q = "MSFT price? PGEN above $1?"
    print(agent.run(q).content)
    

    With two questions in one prompt the agent calls the tool twice and summarizes both results.

    The third example skips local tools entirely and pulls them from a remote MCP server. Agent Framework ships an MCP client, so any compliant server is a tool source with no glue code. The Microsoft Learn MCP server at learn.microsoft.com/api/mcp is public and Streamable-HTTP:

    import asyncio
    from agent_framework.mcp import (
        MCPStreamableHTTPClient,
    )
    
    URL = (
        "https://learn.microsoft.com"
        "/api/mcp"
    )
    
    async def main():
        mcp = MCPStreamableHTTPClient(
            url=URL,
        )
        async with mcp:
            tools = await mcp.list_tools()
            agent = ChatClientAgent(
                chat_client=client,
                instructions=(
                    "MS Learn assistant. "
                    "Use the MCP tools."
                ),
                tools=tools,
            )
            q = (
                "Front Door Standard "
                "vs Premium tiers?"
            )
            r = await agent.run_async(q)
            print(r.content)
    
    asyncio.run(main())
    

    Swap the URL for GitHub’s, Atlassian’s, or your own private one – the rest of the code is unchanged.

    Two gotchas worth knowing: agent-framework==1.0 moved instructions to the ChatClientAgent constructor, so preview code that set it on ChatClientAgentOptions will break. And if a model deployment rejects parallel tool calls, pass parallel_tool_calls=False on the agent constructor.


  • Azure updates: weeks 21-22, 2026

    Azure shipped a batch of updates over the last two weeks. Here are the five worth more than a line.

    Microsoft Agent Framework 1.0 GA

    Microsoft Agent Framework reached 1.0 GA on May 21 for both .NET and Python. The 1.0 release commits Microsoft to a stable API surface with long-term support. Agent Framework consolidates work that was previously split across Semantic Kernel and AutoGen.

    One breaking change from preview to GA: the Instructions setting moved off ChatClientAgentOptions and onto the ChatClientAgent constructor directly.

    Source: Azure update #560982.

    Azure Front Door WebSocket support (public preview)

    Azure Front Door Standard and Premium now support WebSocket connections with no additional configuration. The HTTP handshake is inspected, then the upgraded connection passes through.

    The practical use case is putting Web PubSub, SignalR, or a custom WebSocket workload behind Front Door for global edge presence and WAF inspection at the handshake. Two things to know: WAF rules apply only to the handshake, not to the open connection; and Web PubSub is not on Front Door Premium’s supported Private Link origin list, so a Standard Internal Load Balancer or Application Gateway is needed between Front Door and a private Web PubSub endpoint.

    Source: Azure Front Door WebSocket (Microsoft Learn).

    P2S User Groups and IP address pools (GA)

    Azure VPN Gateway Point-to-Site connections can now assign IP addresses from different pools based on the user’s Entra ID group membership. Prior to this GA, segmenting VPN users by role required parallel gateways or NSG rules keyed off unpredictable IP ranges.

    The mechanism: define User Groups on the gateway, map each group to an address pool, and set a priority for users that match multiple groups. Downstream NSGs can then key off per-group subnets.

    Source: Azure update #564460.

    Entra-only identities with Azure Files (GA)

    Azure Files SMB shares can now authenticate against Microsoft Entra ID alone, without AD DS, Entra Domain Services, or hybrid identity sync.

    For organizations running cloud-only identity, this removes one of the remaining reasons to keep a domain controller running just for file-share access. Hybrid environments still have their own configuration paths.

    Source: Azure update #562359.

    Virtual network flow logs connector with Microsoft Sentinel (GA)

    Azure now offers a native data connector that sends Virtual Network flow logs directly into Microsoft Sentinel. Before this, getting VNet or NSG flow logs into Sentinel for SecOps correlation required a custom ingestion pipeline, typically built on Storage, Event Hub, and a Function App or Logic App.

    For Sentinel deployments that already ingest the other Azure first-party signal sources (Activity Log, Entra ID sign-ins, Defender for Cloud), this adds network-layer telemetry without parallel ingestion infrastructure. Typical detection use cases include lateral movement, anomalous east-west flows, and exfiltration over uncommon ports.

    The connector is GA, not preview.

    Source: Azure update #564689.


  • Running eclipse-mosquitto in an Azure IoT Edge Module

    I was looking to run a MQTT broker on an IoT edge device, allowing devices to communicate locally with each other. The most important thing is to modify the “Container Create Options” so that the container’s port can be seen on the local network. Also I want to use the no auth mosquitto config (it is in development), so I overrode the cmd section.

    With those changes, the device was able to pull the standard docker image “eclipse-mosquitto:latest” and it started running on the local device and the exposed 1883 port was available for local devices

    {
      "NetworkingConfig": {
        "EndpointsConfig": {
          "host": {}
        }
      },
      "ExposedPorts": {
        "1883/tcp": {}
      },
      "HostConfig": {
        "PortBindings": {
          "1883/tcp": [
            {
              "HostPort": "1883"
            }
          ]
        },
        "NetworkMode": "host"
      },
      "Cmd": [
        "/usr/sbin/mosquitto",
        "-c",
        "/mosquitto-no-auth.conf"
      ]
    }
    

  • How to find the Raspberry Pi model

    Note to self:

    How to find the Raspberry Pi Model :

    cat /proc/device-tree/model
    

  • Azure Functions (PowerShell) : Find the current FunctionApp and Function names

    I was writing an Azure Function in PowerShell. I wanted to know the name of the current FunctionApp and the current Function. Since it was a timer triggered function, I couldn’t pass the names. It took me a while to to figure this out. There may be other ways, but this only way I could find it. Again, this is in PowerShell

    if ($ENV:AZURE_FUNCTIONS_ENVIRONMENT  -eq "Development"){
      $CurrentFunctionAppName=$($PSScriptRoot.split("/")[-2])
      $CurrentFunctionName= $($PSScriptRoot.split("/")[-1])
    }
    else {
      $CurrentFunctionAppName=$($ENV:WEBSITE_SITE_NAME)
      $CurrentFunctionName= $($PSScriptRoot.split("/")[-1])
    }
    

    We need a conditional is as the method is different if you are running local vs in azure.


  • Quickly find that new Raspberry Pi’s address

    Saw this the other day, and I wanted to post it, so that I can remember it.

    How to find a Raspberry Pi’s DHCP address:

    arp -na | grep -i "b8:27:eb"
    


  • Quick 1 liner to download your Azure Billing Statements

    Simple and quick – I had never done it before. I was working on a different script, but I realized that I have never downloaded my billing statements from azure. Simple 1 liner to download them:

    foreach ($invoice in $(Get-AzBillingInvoice -GenerateDownloadUrl)){irm $invoice.Downloadurl -OutFile "$($invoice.Name).pdf"}
    

    Hope that helps someone!