feat(agents): hosted agent runtimes — stateless execution, Entra ID auth modes, discovery, and tools as workflow tasks - #1582
Open
v1r3n wants to merge 10 commits into
Open
feat(agents): hosted agent runtimes — stateless execution, Entra ID auth modes, discovery, and tools as workflow tasks#1582v1r3n wants to merge 10 commits into
v1r3n wants to merge 10 commits into
Conversation
…eployments
AzureFoundryAgentClient stored per-run state (endpoint, runId, token provider)
in an in-memory ConcurrentHashMap keyed by threadId. In multi-replica server
deployments the 5-second status-poll callback could arrive on a different pod
than the one that ran startAgent, causing "No execution found" failures on every
execution after the first.
Fix: startAgent now returns a compound executionId that base64-encodes the
non-sensitive run context {threadId, runId, endpoint, assistantId, apiVersion}.
getAgentStatus decodes this to reconstruct the Azure API call on any pod and
re-authenticates using the original task credentialRef, which is available in
the ConductorAgentRequest passed by the updated ConductorAgentDelegate.
Interface change: ConductorAgentClient.getAgentStatus(String) gains a second
ConductorAgentRequest parameter. All five implementations and both test fakes
are updated. ServiceConductorAgentClient ignores the new parameter (AgentService
has its own persistent state). BedrockAgentClient signature is updated; its
in-memory model is unchanged pending a follow-up — the stateless fix for Bedrock
requires a similar compound-executionId approach.
The respondContexts map is retained for respond() and cancelAgent(), which
receive no credentialRef and therefore cannot re-authenticate on a different
replica; single-turn agents (the common case) no longer use it.
…RunStore SPI Following the same pattern as ServiceConductorAgentClient -> AgentService, run context (threadId, runId, endpoint, credentialRef, scope) is now persisted in an injected AzureAgentRunStore rather than encoded in the executionId. The default store is in-memory (InMemoryAzureAgentRunStore, @ConditionalOnMissingBean); HA deployments can substitute a Redis or DB-backed implementation, mirroring how orkes-conductor overrides SkillMetadataDAO and SkillPackageStore. This also removes the previous same-pod limitation on respond() and cancelAgent(): by storing credentialRef in the run context, any replica can re-authenticate and handle multi-turn and cancellation requests without in-process state.
Brings the branch up to date with main (96 commits) and resolves the overlap: - AzureFoundryAgentClient: kept the stateless rewrite, and ported main's 52ab3da fix into AssistantsRunApi.extractText, where that logic now lives. Assistants with code interpreter return an image_file part ahead of the text part, so content[0].text was empty for them. Covered by a new test. - Test fixtures now type their content parts, as the real Assistants API does. - a2a-integration.md: kept main's wording, corrected the runtime list. - conductor-agents.md, mkdocs.yml, .gitignore: took main's versions; the docs nav entry was re-added under main's restructured Agents > Build section.
…ssumeRole, OBO Combines PR #1447 into this branch. Where the two overlapped, #1447's auth and discovery work is ported onto the stateless client structure here rather than the stateful one it was written against. Azure auth (new AzureFoundryAuth): - Four credential modes, first match wins: api-key header, service principal, user-assigned managed identity, default credential chain. A deployment on managed identity now needs no credentialRef at all. - Caller identity (OBO): exchanges the caller's Entra token for a Foundry-scoped one. Never cached — it belongs to a person, not the deployment — and falls back to credential auth when the service principal to exchange it is absent. - Scope follows the endpoint (ai.azure.com / ml.azure.com / cognitiveservices), overridable, and resolved only on a cache miss so a poll reads no secrets. - AssistantsRunApi now takes an AssistantsAuth rather than a bearer string, since Azure may authenticate by api-key header. OpenAI supplies a bearer key. Discovery: agents visible to a credential appear in the agent list — a secret with an 'endpoint' key lists Azure agents, one with 'region' lists Bedrock. Both clients gained listExternalAgents/getExternalAgentDef; AgentService scans secrets for them. Bedrock: AssumeRole (roleArn, roleSessionName, externalId) alongside static keys and the default chain, and the SDK client cache key no longer resolves secrets to compute. agentUrl as a top-level field for both: Azure splits a trailing /assistants/asst_x or /agents/NAME off as the agent; Bedrock parses bedrock://AGENTID/ALIASID?region=. Kept from this branch where they conflicted: the stateless execution model, the parallel-tool contract, and autoRunTools. Kept from main: the agent_capabilities to tags mapping, which #1447 dropped incidentally. Tests: the Azure suite moved to api-key auth so it runs offline — the Identity SDK uses its own HTTP stack, so an OkHttp interceptor no longer catches token calls. New AzureFoundryAuthTest covers mode selection and the OBO exchange against a mock Entra endpoint. Their ITs updated for the two-arg getAgentStatus. UI: took #1447's two genuine fixes — dropped 'unresolved' from the task card, since a workflow registered outside the editor never has a snapshot, and made the live input's agentType authoritative over a lagging snapshot. Provider logos, filter chips, and the caller-identity toggle come in on top of the five-runtime typing already here.
Completes the #1447 fold-in. Foundry serves three APIs behind one agentType and they do not share a protocol; only the classic Assistants surface was handled. - Model inference (chat completions) and a project's Responses API both answer inside the start call, so they report a terminal state through ConductorAgentStartResponse rather than being polled — the same mechanism Bedrock uses, which makes them stateless without a run to remember. - A project agent's own instructions and tools are read from its definition and forwarded, so web search, code interpreter and file search actually run. code_interpreter is wrapped in the container object the Responses API requires. - getAgentStatus reports terminal for both, and respond rejects them with a clear message rather than quietly issuing thread operations against an endpoint that has no threads. rawConfig.surface (assistants | responses | inference) overrides the hostname inference. #1447 classified purely by hostname, which silently misroutes sovereign clouds (.azure.us, .azure.cn), private endpoints and proxies to the classic path — and made the behaviour untestable against a local server.
The suite runs on api-key auth, so nothing reaches an Entra token endpoint through OkHttp any more — the Identity SDK uses its own HTTP stack. The interceptor was still installed but no longer asserted on, and its comment described an exchange the client no longer performs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request type
Changes in this PR
Makes the hosted-platform agent runtimes correct across replicas, implements the OpenAI
Assistants runtime, and lets Conductor run the tools a hosted agent asks for as real workflow
tasks.
Supersedes #1446 and #1447. #1446 added an
AzureAgentRunStoreSPI to make the status pollstateless; this removes the need for a store at all, so that SPI is gone. #1447's auth modes,
agent discovery, AssumeRole, on-behalf-of hooks and UI are all folded in here — ported onto the
stateless client structure rather than the stateful one they were written against.
Hosted agents no longer keep per-run state
AzureFoundryAgentClientkeptendpoint,runIdand the token provider in an in-processConcurrentHashMap. A status poll routed to another replica hit "No execution found", so everyexecution after the first failed in a multi-replica deployment.
The fix is that there is nothing to remember. The
executionIdis the Azure thread id, and thethread is the conversation: the run to act on is always the newest one on it, which the provider
names on request. Everything else — endpoint, assistant, api version, credential, scope — is
re-derived from the task input Conductor already persists. A poll, a respond, or a cancel now
resolves identically on any replica, with no store to configure.
A status poll still costs one HTTP call: listing the newest run returns its full status, so
asking which run is current is no more expensive than having remembered its id.
OpenAI Assistants implemented; the protocol is now shared
Azure Foundry and OpenAI speak the same thread-and-run API, so that protocol moved into
AssistantsRunApiand both clients became thin layers over it — Azure adds Entra ID clientcredentials and
api-version, OpenAI adds an API key and theOpenAI-Betaheader.OpenAiAssistantsAgentClientwas a stub throwingUnsupportedOperationException; it is now aworking runtime.
Bedrock is stateless too, and no longer leaks
Bedrock has no status API —
InvokeAgentstreams the whole turn, so the agent has finished orblocked on a tool before the call returns. It buffered that result in a map, which meant a poll on
another replica reported a terminal failure for a run that had actually succeeded.
It now reports the outcome directly:
startAgentthroughConductorAgentStartResponse.state, andrespondthrough the new defaultrespondWithStatus, so the result lands in the task outputinstead of a map. Runtimes with a status API return null there and are polled exactly as before.
It also built one
BedrockAgentRuntimeAsyncClientper execution and never removed a finishedone, leaking Netty event loops and a connection pool per agent invocation. One client is now shared
per credential and region, released on shutdown.
Parallel tool calls no longer produce wrong answers
A model may ask for several independent tools in one turn. Only the first was reported, and the
reply was then written as the result for every outstanding call — a reply the provider accepts
and the model reasons from, so a second tool asking for headcount was told the revenue figure. Two
200s, a completed workflow, no error anywhere.
pendingToolsnow carries every call end to end, each is answered by its owntool_call_id, and areply that does not cover them all is rejected rather than padded. Single-tool turns are unchanged.
autoRunTools: the agent's tools become workflow tasksA function tool is one the platform cannot run — you registered only its schema. Previously the
AGENTtask completed withwaiting: trueand the workflow author hand-wired a dispatch branchand a resume task for every agent.
With
autoRunTools: truetheAGENTtask staysIN_PROGRESSwhile each requested tool isscheduled as an ordinary task named after the tool, so a worker already registered for
get_revenueserves it with no configuration. Tools fan out in parallel underFORK_JOIN_DYNAMIC, each with its own retries, timeout and execution history, and the agent isresumed with their results keyed by call id. Another turn asking for tools is simply another batch.
toolTaskNamesoverrides the naming convention.Off by default, so existing hand-wired workflows are untouched, and it degrades cleanly: a
remotely-polled SDK worker has no engine to schedule on, so the tool request is handed back as
before. When a tool exhausts its retries the remaining tools are stopped, the agent run is
cancelled, and the task fails with that tool's reason.
Token exchange per poll
OAuthTokenProvidercaches and refreshes a token, but a new provider was built on every call, soeach 5-second poll paid a full Entra ID round trip plus three secret-store reads. Providers are now
cached per credential and scope with a 10-minute TTL; a 401/403 evicts immediately so a rotated
credential is picked up on the next poll. A steady-state poll now performs no secret read at all.
Folded in from #1447
Four Azure auth modes, first match wins — API key (
api-keyheader, no SDK), service principal,user-assigned managed identity, and the default Azure credential chain. A deployment running on
managed identity now needs no
credentialRefat all. Resolved auth is cached per credential andscope, so a poll performs no secret-store read; a 401/403 evicts it immediately.
Running as the caller.
useCallerIdentity: trueexchanges the triggering user's Entra ID token,via the OAuth 2.0 on-behalf-of grant, for one scoped to Foundry — so the agent sees only what that
person can. Their own token never reaches Foundry, and the exchanged token is never cached, since it
belongs to a person rather than the deployment. Without an SSO-supplied assertion or a service
principal to perform the exchange, it falls back to credential auth rather than failing.
All three Foundry surfaces. Foundry is three APIs behind one
agentType: classic Assistants(threads and runs, pollable), a project's Responses API, and model inference. The latter two answer
inside the start call, so they report a terminal state through
ConductorAgentStartResponseinsteadof being polled — the same mechanism Bedrock uses, which makes them stateless with nothing to
remember. A project agent's own instructions and tools are forwarded so its web search, code
interpreter and file search run.
rawConfig.surfaceoverrides the hostname classification. #1447 classified purely by hostname, whichsilently misroutes sovereign clouds (
.azure.us,.azure.cn), private endpoints and proxies to theclassic path.
Agent discovery. A secret with an
endpointkey lists Azure agents; one with aregionkeylists Bedrock agents. They appear in the agent list beside agents defined in Conductor, with no
separate registration. Best effort — a credential that cannot list contributes nothing rather than
breaking the listing.
Bedrock AssumeRole (
roleArn,roleSessionName,externalId) alongside static keys and thedefault chain.
agentUrlas a top-level field for both providers, so every agent type names its location theway A2A does: Azure splits a trailing
/assistants/asst_xor/agents/NAMEoff as the agent,Bedrock parses
bedrock://AGENTID/ALIASID?region=.UI — provider logos and filter chips on the agent list, and a caller-identity toggle on the task
form, on top of the five-runtime typing already here.
Also in here
a2a-integration.mdsaida2awas "the only runtime in OSS today" and that anyother
agentType"is rejected today", while three provider runtimes were registered and working.Corrected, and a new Hosted Platform Agents page documents each runtime's
rawConfigkeys,credential shape, and the tool loop — written from the source.
AgentRuntimeTypewas"a2a" | "conductor", so aFoundry task was labelled an unresolved A2A agent with no name and triggered a doomed
/a2a/agent-carddiscovery call on save. All five runtimes are now typed and badged, hostedagents resolve without remote discovery, and the editor offers them with the right fields.
The execution view lists the tools an agent is waiting on and links to the run executing them.
content[0].textis empty for assistants with code interpreter) into theshared
AssistantsRunApi.extractText, with a regression test, so the rewrite does not lose it.agent_capabilities→tagsmapping, whichfeat(azure-foundry,bedrock): Entra ID auth modes, agent tools forwarding, and AssumeRole support #1447 dropped incidentally, and this branch's stateless execution model. Two things taken from
feat(azure-foundry,bedrock): Entra ID auth modes, agent tools forwarding, and AssumeRole support #1447 in preference to what was here: dropping
unresolvedfrom the task card (a workflowregistered outside the editor never has a snapshot, so it was never a real signal) and treating
the live input's
agentTypeas authoritative over a snapshot that lags a live edit.Alternatives considered
Compound
executionId(the approach in #1446's first commit) base64-encoded the run contextinto the id. Genuinely stateless, but the id is workflow-visible — the delegate mirrors it to
subWorkflowId— so endpoint and assistant leaked into the UI. Encoding only the thread id gets thesame property without that.
A durable
AzureAgentRunStore(#1446's second commit) added an SPI for hosts to implement withRedis or a DB. It shipped an in-process default, so OSS multi-replica stayed broken and every
embedder had infrastructure to supply. Since the provider is the source of truth for run state,
there was nothing that needed storing.
Tool tasks as inline children of the
AGENTtask, rather than a nested workflow, wasinvestigated and deferred. It needs no decider surgery, but the only thing it adds over this is
avoiding one extra workflow execution per tool turn — at the cost of new engine surface for
scheduling arbitrary runtime tasks, first use of the dormant
TaskModel.parentTaskId, and a UIconcept for a task owning children. Worth revisiting if per-turn executions prove costly.
Testing
:conductor-ai:testand:conductor-agentspan:testgreen, spotless cleancompileJava+compileTestJavaclean;mkdocs buildcleanui-next:tsc --noEmitandeslint --quietclean, 882 tests passEntra endpoint), Foundry surfaces and routing, OpenAI, Bedrock, the agent delegate, the tool
dispatcher, plus UI tests for runtime typing, the task form and the execution view
so an OkHttp interceptor no longer intercepts its token calls. Mode selection is asserted without
resolving a token
Not yet exercised against a live provider. The path worth a manual check before merge is an
autoRunToolsagent with two function tools: confirm both tool tasks are picked up in parallel,then terminate the parent mid-flight and confirm the tool workflow terminates with it.