Skip to content

Managed and Prompt Agents - #9687

Closed
Kshitij Chawla (kshitij-microsoft) wants to merge 27 commits into
Azure:mainfrom
kshitij-microsoft:kchawla/azd-managed-harness
Closed

Managed and Prompt Agents#9687
Kshitij Chawla (kshitij-microsoft) wants to merge 27 commits into
Azure:mainfrom
kshitij-microsoft:kchawla/azd-managed-harness

Conversation

@kshitij-microsoft

Copy link
Copy Markdown
Member

No description provided.

…ates

Rename agent manifest ref to agent definition ref, extend prompt harness gating, update agent YAML mapping and API models, and refresh init/show command behavior with tests.
… fixes

Adds RAI policy support end to end: init discovers the policies on the
Foundry account and records one in the environment, and the deploy graph
gains a policy node that lists the account's policies to verify the
declared name. A missing policy is now a warning that falls back to the
built-in default rather than a hard failure -- the account applies its own
default content filters to an agent that names no policy at all, so the
fallback leaves the agent no less filtered than publishing it without
guardrails would.

Also fixes three defects found while exercising the $ref authoring path:

- Agent definitions were decoded into the inline hosted-agent shape before
  the kind was checked, so a prompt agent's string "model" failed to
  unmarshal into the hosted Model struct. The kind gate now runs first.

- rai_policy_name was validated before environment expansion, which
  rejected ${RAI_POLICY_ID} for not being a full ARM resource ID.
  Unexpanded values are now deferred to the deploy path, where the
  expanded value is validated.

- Connection authType "Entra" -- the spelling azd's own documentation and
  scaffolding used -- has never been accepted by the service, whose
  discriminator for that mode is AAD. It is now normalized rather than
  forwarded verbatim into a bad request that lists twenty-one auth types
  without explaining that the two name the same thing.

Replaces the standalone Foundry skills client with the shared skills path,
and updates the agent schema accordingly.
Bring CHANGELOG.md and README.md in line with what the branch actually
ships. The entries were written against the behavior in the code, not
against the design notes, so each one names the failure it replaces
rather than only the feature it adds.

CHANGELOG:
- --kind managed is rejected in favor of --kind prompt with a harness,
  the preview notice now fires on every init path, and the harnessed
  flavor gets its own default agent name so two inits in one folder no
  longer collide on the Foundry agent identity.
- RAI policy selection during init, the RAI_POLICY_ID indirection that
  keeps a scaffold portable across subscriptions, and the best-effort
  pre-publish verification that names the policy and the account.
- skills/ folders now become azure.ai.skill services owned by the skills
  extension; this extension only attaches the published version.
- connections and toolbox references become sibling Foundry services so
  provision and deploy order live in azure.yaml.
- azure.ai.routine is recognized in uses: with the dependency running
  routine to agent.
- The harness type spelling is github_copilot_preview throughout.

README: point at the init-time policy picker and --rai-policy from the
RAI section, which previously implied hand-editing was the only path.
A branch-wide sweep with deadcode, staticcheck, golangci-lint and cspell
turned up two findings introduced by this branch. One was a latent bug.

service_target_prompt.go: resolvePromptWorkspaceFromAzure assigned
p.credential, a concrete *azidentity.AzureDeveloperCLICredential, into an
azcore.TokenCredential and then compared that interface to nil. Prompt
agents skip the hosted credential-init path, so p.credential is always
nil there -- but a nil concrete pointer boxed into an interface yields an
interface that is non-nil while carrying a nil pointer. The comparison
never succeeded, the promptCredential() fallback never ran, and a typed-nil
credential was handed to armresources.NewClient. The nil check now happens
on the concrete pointer before the assignment, so workspace discovery
actually falls back instead of relying on the deferred recover above it to
swallow the result. No other assignment into azcore.TokenCredential has the
same shape.

deploy.go: the deprecated deploy command reassigned extCtx from
ensureExtensionContext and never read it. ensureExtensionContext is pure,
so the call was dead. The parameter is kept for symmetry with the other
constructors in root.go and the doc comment now says why it is unused.

resource_services_test.go: gosec G301 -- the test bundle directory was
created 0o755 and is now 0o750.

cspell.yaml: allow the fifteen terms the branch introduces (prompt-agent
graph, skills, connection and policy vocabulary) so the extension spell
check passes.

samples_test.go is removed. It walked a samples/ tree at the extension
root that was never committed, so it would have failed in CI on every run
while passing locally off untracked files.

Verified clean on all three touched modules: gofmt -s, go fix -diff,
go build, go test, golangci-lint v2.11.4, staticcheck 2026.1,
cspell 8.13.1 and the copyright header check.
Three related parse gaps in the prompt agent manifest, all with the same
shape: an author writes something wrong and azd deploys anyway.

`harness:` changed from a bare string to a block during this work, but
nothing handled the old form. An author carrying a manifest forward got
go-yaml's "cannot unmarshal !!str into agent_yaml.PromptHarness", which
names a Go type and no fix. It now reports the block to write, and
upgrades `ghcp` to `github_copilot_preview` in that suggestion so the
two changes are fixed in one pass rather than one after the other.

`harness.type: ghcp` in the block form was forwarded verbatim, so the
rename was only enforced on the `--harness` flag -- the fourth instance
on this branch of the interactive path validating and the declarative
path not. Only the name azd itself renamed is rejected; an unrecognized
harness still passes through, which is the documented reason there is no
allowlist.

The `harness:` and `memory:` blocks were decoded non-strictly, so
`builtin_tool:` for `builtin_tools:` bound nothing and left every
built-in capability enabled. Both now reject unknown keys. This is
scoped to the blocks azd interprets, not applied at the top level:
validateAgentServiceDefinition passes the whole azure.yaml service entry
-- host, project, uses, config -- through the same validator, so a
top-level strict decode would reject every prompt agent. Tools stay
`[]any` and are unaffected, so a tool type newer than this build still
deploys.

Also drops the panic recovery in resolvePromptWorkspaceFromAzure. It
converted a crash into ("", false), which the caller cannot tell apart
from "no workspace exists", so a panic silently became a request to
provision a new workspace -- that is what hid the typed-nil credential
bug fixed in b59d590. deployPromptAgent already recovers at the RPC
boundary and reports it as a deploy error.

Guards two prompt responses that were dereferenced unconditionally,
matching the form already used in delete.go and eval_helpers.go.
The two words added to cli/azd/.vscode/cspell.yaml were not needed. subagents appears in no Go file, and builtin already occurs in core files that pass CI today, so cspell resolves it from its own dictionaries. Reverting keeps this branch out of the shared core config; the extension config covers the terms this work actually introduces.
`azd ai agent init` no longer writes a separate agent.yaml for prompt and
managed agents. The definition -- kind, model, instructions, harness, tools,
memory, connections, policies -- is written inline on the azure.yaml service
entry, which is the shape hosted and voice agents already used, so every agent
kind is authored in one file and `kind: prompt` is what identifies it.

The config.promptAgent block is no longer written either. Every value it
carried (subscription, resource group, workspace, project endpoint) is recorded
in the azd environment by provision and read from there at deploy time, so the
block could only ever hold a copy of the environment or a set of ${VAR}
references pointing back at it. Hosted agents never had an equivalent.

Deploy still accepts a definition in its own file through $ref:, the
agent.yaml/agent.yml convention, or AGENT_DEFINITION_PATH, and still prefers a
promptAgent block when one is present, so existing projects keep working.

Also in this change:

- Resolve prompt agents by `kind: prompt` rather than by the removed config
  block. list, show, invoke, delete and the down hooks all used the block as
  their discriminator and stopped recognizing an inline definition.
- Surface the whole harness block in `azd ai agent show`, not just its type:
  pinned skills, sandbox CPU/memory/idle timeout, and allowed/excluded built-in
  tools. An explicit empty allow list renders as "(none)" so "nothing allowed"
  is distinguishable from "not configured".
- Spell a RAI policy `rai_policy_name` in azure.yaml, matching the service and
  agent.yaml. Inline entries went through the JSON tag, which was camelCase, so
  the same field had two spellings depending on where it was written. The
  legacy key is still read.
- Validate an inline harness/memory block as strictly as an authored one.
  UnmarshalYAML never runs on the inline path, so the typo checks added for
  agent.yaml would have been lost on what is now the default route.
- Set servicePath for prompt agents, which an early return had skipped, so the
  skills/ and vector-assets/ convention folders resolve relative to the service
  directory when the definition is inline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants