Skip to content
Open
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
4 changes: 4 additions & 0 deletions cli/azd/extensions/azure.ai.agents/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release History

## 0.1.42-preview (Unreleased)

- [[#8824]](https://github.com/Azure/azure-dev/pull/8824) Add `activity_protocol` support to `azd ai agent invoke`, alongside the existing `responses` and `invocations` protocols. A plain message is wrapped as a message Activity, `--input-file` sends a complete Activity object, and `--output raw` dumps the response verbatim.

## 0.1.41-preview (2026-06-19)

- [[#8731]](https://github.com/Azure/azure-dev/pull/8731) Improve the post-deploy `Next:` guidance with a stacked layout that puts each command on its own line above its description, adds a blank line between suggestions, and highlights `azd` commands. The new layout applies across deploy, `azd ai agent show`, `init`, and `doctor`. Thanks @therealjohn for the contribution!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ const agentEndpointHint = "run `azd ai agent show` to see the agent endpoint URL
//
// [1] project name (URL-escaped),
// [2] agent name (URL-escaped),
// [3] protocol tail ("invocations" or "openai/responses").
// [3] protocol tail ("invocations", "activity", or "openai/responses").
//
// The "openai/v1/responses" tail is also accepted and rebuilt to the canonical
// query-parameter form when invoked.
var agentEndpointPathRegex = regexp.MustCompile(
`^/api/projects/([^/]+)/agents/([^/]+)/endpoint/protocols/(invocations|openai/v1/responses|openai/responses)/?$`,
`^/api/projects/([^/]+)/agents/([^/]+)/endpoint/protocols/` +
`(invocations|activity|openai/v1/responses|openai/responses)/?$`,
)

// parsedAgentEndpoint describes a deployed agent invocation endpoint.
Expand Down Expand Up @@ -134,6 +135,8 @@ func parseAgentEndpoint(rawURL string) (*parsedAgentEndpoint, error) {
switch protocolTail {
case "invocations":
protocol = agent_api.AgentProtocolInvocations
case "activity":
protocol = agent_api.AgentProtocolActivityProtocol
case "openai/responses", "openai/v1/responses":
protocol = agent_api.AgentProtocolResponses
}
Expand Down Expand Up @@ -187,3 +190,26 @@ func buildInvocationsURL(projectEndpoint, agentName, apiVersion, sid string) str
}
return invURL
}

// buildActivityURL builds the Foundry "activity" protocol URL for an agent.
// When sid is non-empty, an agent_session_id query parameter is appended
// (URL-encoded) so the request routes to the same agent session, mirroring the
// invocations protocol.
//
// NOTE: The activity protocol wire format (path tail `protocols/activity`,
// Activity request body, and response shape) follows the same path convention
// as the responses/invocations endpoints. This is centralized here so it can be
// adjusted in one place if the Foundry contract differs.
func buildActivityURL(projectEndpoint, agentName, apiVersion, sid string) string {
if apiVersion == "" {
apiVersion = DefaultAgentAPIVersion
}
actURL := fmt.Sprintf(
"%s/agents/%s/endpoint/protocols/activity?api-version=%s",
projectEndpoint, agentName, url.QueryEscape(apiVersion),
)
if sid != "" {
actURL += "&agent_session_id=" + url.QueryEscape(sid)
}
return actURL
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ func TestParseAgentEndpoint(t *testing.T) {
wantAgent: "hello",
wantProto: agent_api.AgentProtocolInvocations,
},
{
name: "activity with api-version",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/activity?api-version=v1",
wantProj: "https://acct.services.ai.azure.com/api/projects/proj",
wantAgent: "hello",
wantProto: agent_api.AgentProtocolActivityProtocol,
wantAPIVer: "v1",
},
{
name: "activity without api-version",
raw: "https://acct.services.ai.azure.com/api/projects/proj/agents/hello/endpoint/protocols/activity",
wantProj: "https://acct.services.ai.azure.com/api/projects/proj",
wantAgent: "hello",
wantProto: agent_api.AgentProtocolActivityProtocol,
},
{
name: "empty url",
raw: "",
Expand Down Expand Up @@ -246,6 +261,37 @@ func TestBuildInvocationsURL(t *testing.T) {
})
}

// TestBuildActivityURL verifies the activity URL builder targets the
// .../protocols/activity tail, applies the default api-version, and URL-encodes
// both the api-version and any session id.
func TestBuildActivityURL(t *testing.T) {
t.Parallel()
const projectEndpoint = "https://acct.services.ai.azure.com/api/projects/proj"

t.Run("no session id, default api-version", func(t *testing.T) {
got := buildActivityURL(projectEndpoint, "hello", "", "")
want := projectEndpoint + "/agents/hello/endpoint/protocols/activity?api-version=" +
DefaultAgentAPIVersion
if got != want {
t.Errorf("buildActivityURL = %q, want %q", got, want)
}
})

t.Run("session id is escaped", func(t *testing.T) {
got := buildActivityURL(projectEndpoint, "hello", "v1", "a b/c?d&e")
if !strings.Contains(got, "agent_session_id=a+b%2Fc%3Fd%26e") {
t.Errorf("buildActivityURL did not escape session id: %q", got)
}
})

t.Run("api-version is escaped", func(t *testing.T) {
got := buildActivityURL(projectEndpoint, "hello", "weird value&x=1", "")
if !strings.Contains(got, "api-version=weird+value%26x%3D1") {
t.Errorf("buildActivityURL did not escape api-version: %q", got)
}
})
}

// TestResolveRemoteContext_EphemeralMode exercises the ephemeral branch of
// resolveRemoteContext (--agent-endpoint path). It pins the api-version
// fallback (default applied when the URL omits the parameter) and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -894,7 +894,7 @@ func protocolFromAgentYaml(
"agent.yaml declares only non-invocable protocols: %s",
strings.Join(names, ", "),
),
"azd can only invoke agents using the responses or invocations protocols",
"azd can only invoke agents using the responses, invocations, or activity protocols",
)
case 1:
// Exactly one invocable protocol — but if the agent declares
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ func TestProtocolFromAgentYaml(t *testing.T) {
yaml: "protocols:\n - protocol: invocations\n version: \"1.0\"\n",
wantProto: "invocations",
},
{
name: "single protocol activity_protocol",
yaml: "protocols:\n - protocol: activity_protocol\n version: \"1.0\"\n",
wantProto: "activity_protocol",
},
{
name: "no file",
noFile: true,
Expand Down
24 changes: 19 additions & 5 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ agent name and the second is the message.

Use --input-file/-f to send the contents of a file as the request body
instead of a positional message argument. This is useful for structured
or large payloads with the invocations protocol.
or large payloads with the invocations protocol, or for sending a complete
Activity object with the activity protocol.

Use --local to target a locally running agent (started via 'azd ai agent run')
instead of Foundry.
Expand Down Expand Up @@ -111,6 +112,12 @@ suppressed in raw mode.`,
# Invoke using a specific protocol
azd ai agent invoke --protocol invocations "Hello!"

# Invoke over the activity protocol (wraps the message as a message Activity)
azd ai agent invoke --protocol activity_protocol "Hello!"

# Invoke the activity protocol with a complete Activity object from a file
azd ai agent invoke --protocol activity_protocol -f activity.json

# Invoke with a file as the request body
azd ai agent invoke -f request.json

Expand Down Expand Up @@ -203,7 +210,7 @@ suppressed in raw mode.`,
return exterrors.Validation(
exterrors.CodeInvalidParameter,
fmt.Sprintf("unsupported protocol %q for invocation", flags.protocol),
"supported protocols are: responses, invocations",
"supported protocols are: responses, invocations, activity_protocol",
)
}
}
Expand All @@ -214,7 +221,8 @@ suppressed in raw mode.`,

cmd.Flags().BoolVarP(&flags.local, "local", "l", false, "Invoke on localhost instead of Foundry")
cmd.Flags().StringVarP(&flags.inputFile, "input-file", "f", "", "Path to a file whose contents are sent as the request body")
cmd.Flags().StringVarP(&flags.protocol, "protocol", "p", "", "Protocol to use: responses (default) or invocations")
cmd.Flags().StringVarP(&flags.protocol, "protocol", "p", "",
"Protocol to use: responses (default), invocations, or activity_protocol")
cmd.Flags().IntVar(&flags.port, "port", DefaultPort, "Local server port")
cmd.Flags().IntVarP(
&flags.timeout,
Expand Down Expand Up @@ -360,16 +368,22 @@ func (a *InvokeAction) Run(ctx context.Context) error {
switch protocol {
case agent_api.AgentProtocolInvocations:
return a.invocationsLocal(ctx)
case agent_api.AgentProtocolActivityProtocol:
return a.activityLocal(ctx)
default:
return a.responsesLocal(ctx)
}
}

// Remote: route by protocol.
if protocol == agent_api.AgentProtocolInvocations {
switch protocol {
case agent_api.AgentProtocolInvocations:
return a.invocationsRemote(ctx)
case agent_api.AgentProtocolActivityProtocol:
return a.activityRemote(ctx)
default:
return a.responsesRemote(ctx)
}
return a.responsesRemote(ctx)
}

// emitInvokeSuccessNextStep prints the resolver-driven Next: block after a
Expand Down
Loading
Loading