feat(ai-agents): add --agent-endpoint flag to invoke command#8028
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates azd ai agent invoke so --agent-endpoint can accept a full Foundry agent endpoint URL and derive the remote invocation settings directly from it, including support for running outside an azd project.
Changes:
- Added parsing and validation for full Foundry agent endpoint URLs, plus a new
--agent-endpointflag. - Refactored remote invocation setup to support an “ephemeral” mode that skips azd project resolution and uses values derived from the endpoint URL.
- Added tests for endpoint parsing and upfront flag-conflict validation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go |
Wires in --agent-endpoint, adds remote-context resolution, and updates remote request construction. |
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_test.go |
Adds validation tests for --agent-endpoint conflict handling. |
cli/azd/extensions/azure.ai.agents/internal/cmd/agent_endpoint.go |
Implements endpoint URL parsing/validation and ephemeral session hint output. |
cli/azd/extensions/azure.ai.agents/internal/cmd/agent_endpoint_test.go |
Adds tests for endpoint parsing, agent-name validation, and session hint smoke coverage. |
jongio
left a comment
There was a problem hiding this comment.
resolveRemoteContext cleanly centralizes name/endpoint/api-version resolution. Deferring acquireBearerToken until after body validation avoids unnecessary token round-trips on input errors. URL parser is thorough (scheme, host suffix, port rejection, path structure, agent-name charset, empty api-version).
One gap: ephemeral responses mode creates a conversation via createConversation but doesn't print a --conversation-id continuation hint. See thread on printEphemeralSessionHint.
wbreza
left a comment
There was a problem hiding this comment.
Review Summary
This PR adds an --agent-endpoint flag to azd ai agent invoke, enabling users to invoke Foundry agents from any directory without needing an azd project. The code is well-structured with good URL parsing/validation in a dedicated agent_endpoint.go file. Most prior feedback has been addressed across 6 iterative commits.
Should Fix
1. Empty API version produces malformed request URL
When a user provides an endpoint URL without a ?api-version= query parameter, ApiVersion is set to in the parsed struct.buildResponsesURL()/buildInvocationsURL()then produce?api-version=(key present, empty value). The existing fix rejects explicit?api-version=with no value, but not the absence of the parameter entirely. Consider falling back toDefaultAgentAPIVersionwhenapiVersion` is empty, or rejecting URLs missing the parameter.
2. Ephemeral conversation hint missing from invocationsRemote()
responsesRemote() prints both printEphemeralSessionHint and printEphemeralConversationHint, but invocationsRemote() only prints the session hint. Users invoking with --protocol invocations in ephemeral mode won't see how to continue their conversation — inconsistent UX between the two protocols.
3. Missing test coverage for resolveRemoteContext() and ephemeral integration flow
resolveRemoteContext() is a critical new function that branches between ephemeral and project modes, handles auth, and manages resource cleanup. It currently has no tests. The parsing helpers have good coverage, but the orchestration layer is untested.
4. Tests use raw t.Errorf/t.Fatalf instead of testify assertions
Repo convention uses testify (require/assert). The new agent_endpoint_test.go uses raw Go testing patterns. Should use require.NoError(t, err), require.Equal(t, expected, actual), etc.
5. Credential error messages may expose auth infrastructure details
ephemeralAuthError() uses fmt.Sprintf("failed to get auth token: %v", err) which may surface OAuth endpoint URLs or credential manager internals via %v formatting.
Nitpick
6. Several test functions in agent_endpoint_test.go are missing t.Parallel().
7. Variable url shadows the net/url package import — consider renaming to respURL.
jongio
left a comment
There was a problem hiding this comment.
Addresses my previous feedback - the conversation-id continuation hint is clean and well-tested. The guard logic (skip when user already supplied --conversation-id, skip when no conversation was created) covers the right cases. LGTM.
7d2fc12 to
5948c96
Compare
jongio
left a comment
There was a problem hiding this comment.
Re-approving after the rebase and rework. The config persistence layer (config_store.go) is clean - proper validation, key normalization, legacy fallback with rewrite. The stderr fix for warnIneffectiveResetFlags, delegation to agent_yaml.ValidateAgentName, and the tip messages are all good. All prior findings addressed.
wbreza
left a comment
There was a problem hiding this comment.
Re-Review (post-rebase) — Approve
The PR was rebased with 7 new commits. 5 of 7 original findings are resolved. New commits fix real bugs: broken saveContextValue(..., "invocations"), suppressed log.Printf warnings, and loose agent name validation now delegated to �gent_yaml.ValidateAgentName.
Previous Findings — Status
| Finding | Status |
|---|---|
| Empty API version → malformed URL | ✅ Resolved |
| Conversation hint missing from invocationsRemote() | ✅ Resolved |
| Credential error exposure | ✅ Resolved |
| Missing .Parallel() | ✅ Resolved |
| Variable shadowing | ✅ Resolved |
| Missing | |
| esolveRemoteContext() tests | |
| Raw .Errorf vs testify |
New Findings
🟡 Should Fix
1.
esolveRemoteContext() still has no dedicated tests
This function branches between ephemeral and project modes, manages �zdClient lifecycle, and handles auth. It's the critical orchestration point but remains untested in isolation. Parsing helpers have good coverage; the integration layer does not.
2. warnIneffectiveResetFlags() doesn't account for protocol
--new-conversation is always a no-op for invocations protocol (memory is session-bound), but the warning only fires when there's no parent daemon. A user with a daemon running invocations + --new-conversation gets no warning.
🟢 Nitpick
3. �gent_endpoint_test.go assertion style inconsistency
config_store_test.go correctly uses �ssert.Equal()/�ssert.NotEqual(), but �gent_endpoint_test.go still uses .Fatalf/ .Errorf. Standardizing would improve consistency.
4. No test for URL-encoded invalid agent names
TestParseAgentEndpoint_RejectsInvalidAgentNames tests underscores, length, and hyphens — but not URL-encoded characters like �gent%20name. Likely works via ValidateAgentName() but worth a test case.
|
Thanks for the re-review @wbreza. Status on the remaining items: New #1 New #2 — |
Adds a new --agent-endpoint flag to 'azd ai agent invoke' that accepts the full Foundry agent invocation URL printed by 'azd up' / 'azd deploy' and lets the user invoke a deployed agent from any directory without an azd project on disk. * Parses the URL strictly: requires https, the *.services.ai.azure.com Foundry host, the canonical /api/projects/.../agents/.../endpoint/ protocols/<protocol>[?api-version=...] path, no explicit port, and a non-empty api-version when present. * Derives the protocol (invocations or openai/responses) from the URL and rejects any flags that have no meaning in ephemeral mode (--local, positional name, --port, --protocol, --new-session, --new-conversation). * Body validation runs before bearer-token acquisition so local input errors surface ahead of any auth round-trip. * Prints continuation hints for both server-assigned --session-id and auto-created --conversation-id so users can preserve multi-turn state on the next invoke. * Adds buildResponsesURL / buildInvocationsURL helpers and unit tests covering api-version propagation and URL-encoding of session ids. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- agent_endpoint.go: replace ad-hoc segment-by-segment path validation with a single regex match (matches existing projectResourceIdRegex style elsewhere in the package). - agent_endpoint.go: introduce agentEndpointHint constant and use it everywhere the previous 'pass the agent endpoint printed by azd up or azd deploy' message appeared. Now points users at 'azd ai agent show', which persistently prints the endpoint URL. - invoke.go: collapse validateAgentEndpointFlags' six-case switch into a generic table-driven loop over disallowed flags. - agent_endpoint_test.go: update unknown_protocol_tail expectation to match the unified regex error message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- invoke.go: restore the `Invocation:` print line that was lost during the rebase. Previously, `invocationsRemote` always printed the `x-agent-invocation-id` header so users could correlate the call for tracing. The rebased version only persisted it (and only in project mode), so `--agent-endpoint` callers and project-mode callers both lost the visible handle. Restore the print and keep the persist as an extra step in project mode. - agent_endpoint.go: reject `%2F` (or other encoded path separators) inside the project segment of `--agent-endpoint`. The regex captures `[^/]+` against the escaped path, so an encoded slash slipped through validation and `url.PathUnescape` then materialized a literal `/` in the project name. Add a `ContainsAny(name, "/\\")` check to match the strictness already applied to the agent segment. Add a test case covering `proj%2Fother`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
After PR Azure#8034 the session/conversation store is keyed by an endpoint-derived agentKey in global UserConfig (env-independent). This wires --agent-endpoint into that store so ephemeral invokes auto-resume across calls. Changes: - Add buildEphemeralAgentKey: a stable, query-string-free key derived from the parsed projectEndpoint+agentName. Distinct '/ephemeral' suffix so it never collides with project-mode '/remote' keys. - resolveRemoteContext (ephemeral): best-effort attach the parent azd daemon and set rc.agentKey. Standalone runs (no daemon) silently fall back to no-persistence. - responsesRemote / invocationsRemote: drop the local agentKey computation; use rc.agentKey. Tighten OpenAPI-spec fetch to project mode only (no on-disk side effect for ephemeral). - Drop --new-session / --new-conversation from validateAgentEndpointFlags so users can reset stored IDs in ephemeral mode. Add warnIneffectiveResetFlags to log a no-op warning when standalone. - Continuation hints now require resp.StatusCode<400 so failed invokes don't tell users to continue a never-created conversation (review feedback). - Hint gate widened to (agentKey == '' || azdClient == nil) so it fires whenever persistence is genuinely unavailable, not just when the daemon is missing. Tests: TestBuildEphemeralAgentKey covers URL-variant stability (canonical, trailing-slash, mixed case host) and the project-mode key-collision contract. The two dropped --new-* validation cases removed from TestAgentEndpointFlagValidation. Live-verified against the deployed responses agent: invokes 1+2 share session+conversation IDs and the agent recalls prior turns; invoke 3 with --new-session --new-conversation produces fresh IDs; invoke 4 with a URL variant (no ?api-version) hits the same persisted entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The standalone-mode hint code (printEphemeralSessionHint / printEphemeralConversationHint) only fired when the parent azd daemon was unreachable -- a path that does not occur in normal user flow (�zd ai agent invoke ... always spawns the daemon). With persistence working under Azure#8034 those hints were essentially dead code. Remove both helpers (and their tests / unused captureStdout helper / net/http import) and replace with a single concise tip printed after a successful invoke when persistence is active in both responsesRemote and invocationsRemote: (tip: pass --new-session or --new-conversation to reset; see `azd ai agent invoke --help`) Tests + lint clean. Live verified end-of-output ordering against hello-world-python-responses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The post-invoke tip and reset-flag handling now match each protocol's actual memory model: - Responses protocol keeps the existing tip mentioning both --new-session and --new-conversation (it uses the Foundry Conversations API for multi-turn memory). - Invocations protocol prints a tip that only mentions --new-session, since memory is bound to the session and --new-conversation has no observable effect on this path. - If the user does pass --new-conversation while invoking an invocations endpoint, a stderr note explains that the flag is a no-op for this protocol and points to --new-session instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- agent_endpoint.go: agent-name validation now delegates to agent_yaml.ValidateAgentName so --agent-endpoint enforces the same deployable-name format as the rest of the extension. Previously underscores and unbounded lengths slipped through local validation only to fail later as 404s. The bespoke isValidAgentNameSegment helper and its unit test are removed; a new TestParseAgentEndpoint_RejectsInvalidAgentNames covers underscore, length>63, and leading/trailing hyphen rejection. - invoke.go (warnIneffectiveResetFlags): switched from log.Printf to fmt.Fprintln(os.Stderr, ...). The extension silences the standard logger unless debug mode is enabled (setupDebugLogging redirects to io.Discard), so the previous warning never reached users in standalone --agent-endpoint mode. - invoke.go (invocationsRemote): removed the saveContextValue(..., "invocations") call. validateStoreField only allows "sessions" and "conversations", so the persistence call always failed silently. The invocation ID is still printed for trace correlation; we just no longer pretend to persist it. - config_store.go: rewrote a doc comment to use "scopes" instead of "lifecycles" so the cspell pipeline passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per PR review (trangevi): unify ephemeral and project-mode key builders. buildAgentKey(ep, name, '', false) yields the same canonical shape used elsewhere '<ep>/agents/<name>/versions/latest/remote' and inherits the segment validation logic for free. - Remove buildEphemeralAgentKey helper (config_store.go) - Update sole call site in invocations setup (invoke.go) - Drop now-redundant ephemeral-specific tests; URL-variant stability remains covered by TestNormalizeEndpoint_StripScheme and TestBuildRemoteAgentKeyFromEndpoint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Per PR review (trangevi): the example used the /protocols/invocations URL paired with a plain "Hello!" body, but most invocations samples expect a JSON request body. Switch the example to the responses-protocol URL (/protocols/openai/responses) which matches the plain-string body shape and aligns with the other "Hello!" examples in this help block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The warning only fired when running the extension binary standalone with --agent-endpoint and a reset flag (no parent azd daemon). End-user invokes always go through the host and persist via gRPC, so the warn was effectively dead for the supported flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename url locals to respURL/convURL to avoid shadowing the net/url package import (invoke.go: responsesRemote and createConversation). - Add t.Parallel() to the 4 functions in agent_endpoint_test.go. - Add TestResolveRemoteContext_EphemeralMode covering the api-version default fallback (when URL omits ?api-version=) and explicit override, plus name/projectEndpoint/agentKey propagation. Pins the existing safe behavior end-to-end. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
854e901 to
20d23e4
Compare
|
/check-enforcer override |
Summary
Adds
--agent-endpointtoazd ai agent invokeso users can invoke a deployed Foundry agent from any directory without needing an azd project,azure.yaml, or environment.azd ai agent invoke \ --agent-endpoint <endpoint-url> \ 'Hello!'The endpoint is the same URL printed by
azd ai agent showfor a deployed agent.Behavior
--protocolneeded.AzureDeveloperCLICredential(runazd auth loginfirst), consistent with the rest of the extension.--new-sessionor--new-conversationto reset.--helpfor the reset flags. The tip is protocol-aware:(tip: pass --new-session or --new-conversation to reset; see azd ai agent invoke --help)(tip: pass --new-session to reset; see azd ai agent invoke --help)multi-turn memory on this protocol is bound to the session, so--new-conversationis not advertised.--new-conversationwhile invoking an invocations endpoint, anote:is printed to stderr explaining that the flag has no effect for this protocol and pointing at--new-sessioninstead. The flag is still accepted (no error) so existing scripts keep working.x-agent-invocation-id) returned by the invocations protocol is printed for trace correlation, matching the local-invoke behavior.Changes
--agent-endpointflag onazd ai agent invoke. A single regex parses/api/projects/<proj>/agents/<name>/endpoint/protocols/(invocations|openai/responses)and derivesprojectEndpoint, agentname,protocol, andapiVersion(from theapi-versionquery parameter) in one pass.%2F,%5C) inside the project segment so the parsed project name is stable.--local, a positional agent name,--port,--protocol. Each is rejected with a targeted suggestion.--new-session/--new-conversationare accepted (they reset the persisted IDs).Example
with responses endpoint:

with invocations endpoint:

when --new-conversation flag is added with invocations endpoint, a warning/note is shown:
