diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_endpoint.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_endpoint.go index eb229a7df2d..ed7dab7be46 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_endpoint.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/agent_endpoint.go @@ -222,6 +222,23 @@ func buildInvocationsURL(projectEndpoint, agentName, apiVersion, sid string) str return invURL } +func buildInvocationRetrievalURL(projectEndpoint, agentName, invocationID, apiVersion, sid string) string { + if apiVersion == "" { + apiVersion = DefaultAgentAPIVersion + } + base := fmt.Sprintf( + "%s/agents/%s/endpoint/protocols/invocations/%s", + projectEndpoint, + agentName, + url.PathEscape(invocationID), + ) + query := url.Values{"api-version": []string{apiVersion}} + if sid != "" { + query.Set("agent_session_id", sid) + } + return base + "?" + query.Encode() +} + // buildA2AInvokeURL builds the Foundry "a2a" 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, matching the invocations protocol. diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go index 5da590b27db..79de0ab309a 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go @@ -131,9 +131,11 @@ Use --resumable with the Responses protocol to start work that continues running the service if this command disconnects. The command remains attached until the work finishes. Add --no-wait to detach as soon as the service acknowledges the background work. Use --resume to reconnect to saved work, --steer with input to revise active work -or start the next resumable turn after completion, and --cancel to cancel saved work. In -multi-agent projects, pass the agent name positionally. Resumable operations are remote-only, -do not support raw output, and cannot be combined with --timeout.`, +or start the next resumable turn after completion, and --cancel to cancel saved work. For +remote Invocations agents, message-free --resume performs one best-effort GET of the latest +saved invocation without polling or interpreting its lifecycle. In multi-agent projects, +pass the agent name positionally. Saved-work operations are remote-only, do not support raw +output, and cannot be combined with --timeout.`, Example: ` # Invoke the remote agent on Foundry (auto-detects agent from azure.yaml) azd ai agent invoke "Hello!" @@ -165,7 +167,7 @@ do not support raw output, and cannot be combined with --timeout.`, # while remaining attached until it finishes azd ai agent invoke --resumable "Run the long task" - # Start resumable work and detach after the service acknowledges it + # Start background work and detach after the service acknowledges it azd ai agent invoke --resumable --no-wait "Run the long task" # Resume, steer, or cancel saved resumable work @@ -177,6 +179,9 @@ do not support raw output, and cannot be combined with --timeout.`, azd ai agent invoke my-agent --resume azd ai agent invoke my-agent --cancel + # Retrieve the latest saved Invocation once (no polling or replay guarantee) + azd ai agent invoke my-agent --protocol invocations --resume + # Start a new session (discard conversation history) azd ai agent invoke --new-session "Hello!" @@ -279,15 +284,15 @@ do not support raw output, and cannot be combined with --timeout.`, if flags.local { return exterrors.Validation( exterrors.CodeInvalidParameter, - "resumable operations are supported only for remote Responses agents", - "remove --local and use a deployed Responses agent", + "saved-work operations are supported only for remote agents", + "remove --local and use a deployed agent", ) } if flags.outputFmt == outputRaw { return exterrors.Validation( exterrors.CodeInvalidParameter, - "--output raw is not supported with resumable operations", - "remove --output raw so azd can manage the Response identity and cursor", + "--output raw is not supported with saved-work operations", + "remove --output raw so azd can manage saved operation state", ) } } @@ -348,7 +353,12 @@ do not support raw output, and cannot be combined with --timeout.`, "Start resumable work that continues in the service if the command disconnects; remain attached until it finishes", ) cmd.Flags().BoolVar(&flags.noWait, "no-wait", false, "Detach after the service acknowledges the resumable work") - cmd.Flags().BoolVar(&flags.resume, "resume", false, "Reconnect to saved background work") + cmd.Flags().BoolVar( + &flags.resume, + "resume", + false, + "Reconnect to saved background work or retrieve the latest saved Invocation once", + ) cmd.Flags().BoolVar( &flags.steer, "steer", @@ -445,7 +455,7 @@ func validateInvokeOperationFlags(cmd *cobra.Command, flags *invokeFlags) error return exterrors.Validation( exterrors.CodeInvalidParameter, "--resume and --cancel do not accept a message or --input-file", - "remove the input to reconnect to or cancel the saved Response", + "remove the input; --resume reconnects to saved work and --cancel cancels saved Responses", ) } @@ -464,7 +474,7 @@ func validateInvokeOperationFlags(cmd *cobra.Command, flags *invokeFlags) error return exterrors.Validation( exterrors.CodeConflictingArguments, "--timeout is not supported with --resume, --steer, or --cancel", - "remove --timeout; attached background work has no overall timeout", + "remove --timeout; saved-work operations manage request timing internally", ) } } @@ -579,11 +589,15 @@ func (a *InvokeAction) Run(ctx context.Context) error { // intends to prevent. if (a.flags.resumable || a.flags.resume || a.flags.steer || a.flags.cancel) && protocol != agent_api.AgentProtocolResponses { - return exterrors.Validation( - exterrors.CodeInvalidParameter, - fmt.Sprintf("resumable operations are not supported with the %s protocol", protocol), - "use a deployed Responses agent or remove the resumable operation", - ) + invocationsGet := protocol == agent_api.AgentProtocolInvocations && a.flags.resume && + !a.flags.resumable && !a.flags.steer && !a.flags.cancel + if !invocationsGet { + return exterrors.Validation( + exterrors.CodeInvalidParameter, + fmt.Sprintf("resumable operations are not supported with the %s protocol", protocol), + "use a deployed Responses agent or remove the resumable operation", + ) + } } if len(a.clientHeaders) > 0 && protocol == agent_api.AgentProtocolA2A { @@ -609,6 +623,9 @@ func (a *InvokeAction) Run(ctx context.Context) error { // Remote: route by protocol. switch protocol { case agent_api.AgentProtocolInvocations: + if a.flags.resume { + return a.invocationsResumeRemote(ctx) + } return a.invocationsRemote(ctx) case agent_api.AgentProtocolA2A: return a.a2aRemote(ctx) @@ -1799,15 +1816,25 @@ func (a *InvokeAction) invocationsRemote(ctx context.Context) error { ttfb := time.Since(invokeStart) defer resp.Body.Close() - // Print the invocation ID if the agent returned one. We do not persist it - // to the per-user config: the config store only supports the "sessions" - // and "conversations" maps (see validateStoreField), and invocation IDs - // are not used to drive any subsequent invoke -- they are emitted purely - // for trace correlation. - if !raw { - if invID := resp.Header.Get("x-agent-invocation-id"); invID != "" { - fmt.Printf("Invocation: %s\n", invID) + // Capture the invocation ID for diagnostics and best-effort later retrieval. + // The AgentServer adapter normally returns the ID in a header; asynchronous + // implementations may return it only in the 202 response body. + invocationID := resp.Header.Get("x-agent-invocation-id") + if invocationID == "" && resp.StatusCode == http.StatusAccepted { + responseBody, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return fmt.Errorf("read invocation acceptance response: %w", readErr) + } + resp.Body = io.NopCloser(bytes.NewReader(responseBody)) + var accepted struct { + InvocationID string `json:"invocation_id"` } + if json.Unmarshal(responseBody, &accepted) == nil { + invocationID = accepted.InvocationID + } + } + if !raw && invocationID != "" { + fmt.Printf("Invocation: %s\n", invocationID) } // Always capture session state from response headers (needed even in raw mode @@ -1818,6 +1845,20 @@ func (a *InvokeAction) invocationsRemote(ctx context.Context) error { } captureResponseSession(ctx, rc.azdClient, agentKey, sid, resp, sessionLabel) + if resp.StatusCode < 400 && rc.azdClient != nil && agentKey != "" && invocationID != "" { + effectiveSessionID := sid + if assigned := resp.Header.Get("x-agent-session-id"); assigned != "" { + effectiveSessionID = assigned + } + if err := newInvocationStateStore(rc.azdClient).Save(ctx, agentKey, savedInvocation{ + InvocationID: invocationID, + SessionID: effectiveSessionID, + APIVersion: rc.apiVersion, + }); err != nil { + log.Printf("warning: failed to save invocation %s for later retrieval: %v", invocationID, err) + } + } + sessionCode := resp.Header.Get("x-adc-response-details") if err := handleInvocationResponse( ctx, diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background_test.go index 0ea34fe8568..fd68668ab59 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background_test.go @@ -1130,7 +1130,7 @@ func TestInvokeCommandBackgroundValidation(t *testing.T) { { name: "rejects local", args: []string{"--resumable", "--local", "hello"}, - want: "supported only for remote Responses agents", + want: "saved-work operations are supported only for remote agents", }, { name: "rejects explicit invocations protocol", @@ -1170,6 +1170,113 @@ func TestInvokeCommandBackgroundValidation(t *testing.T) { } } +func TestInvokeCommandRejectsInvocationsResumeWithInput(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + }{ + { + name: "message", + args: []string{"agent", "revised requirements", "--protocol", "invocations", "--resume"}, + }, + { + name: "input file", + args: []string{"--protocol", "invocations", "--resume", "--input-file", "request.json"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cmd := newInvokeCommand(nil) + cmd.SetArgs(tt.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := cmd.Execute() + localErr, ok := errors.AsType[*azdext.LocalError](err) + require.True(t, ok) + assert.Equal(t, exterrors.CodeInvalidParameter, localErr.Code) + assert.Equal(t, "--resume and --cancel do not accept a message or --input-file", localErr.Message) + assert.Equal( + t, + "remove the input; --resume reconnects to saved work and --cancel cancels saved Responses", + localErr.Suggestion, + ) + }) + } +} + +func TestInvokeCommandRejectsInvocationResumeWithAgentEndpoint(t *testing.T) { + isolateFromAzdDaemon(t) + + cmd := newInvokeCommand(nil) + cmd.SetArgs([]string{ + "--resume", + "--agent-endpoint", + "https://acct.services.ai.azure.com/api/projects/proj/agents/test-agent/endpoint/protocols/invocations", + }) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := cmd.Execute() + localErr, ok := errors.AsType[*azdext.LocalError](err) + require.True(t, ok) + assert.Equal(t, exterrors.CodeInvalidParameter, localErr.Code) + assert.Equal(t, "Invocations --resume is not supported with --agent-endpoint", localErr.Message) + assert.Equal(t, "run from an azd project so the saved Invocation can be resolved", localErr.Suggestion) +} + +func TestInvokeCommandInvocationResumeDiagnostics(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + extCtx *azdext.ExtensionContext + args []string + message string + suggestion string + }{ + { + name: "local", + args: []string{"--protocol", "invocations", "--resume", "--local"}, + message: "saved-work operations are supported only for remote agents", + suggestion: "remove --local and use a deployed agent", + }, + { + name: "raw output", + extCtx: &azdext.ExtensionContext{OutputFormat: outputRaw}, + args: []string{"--protocol", "invocations", "--resume"}, + message: "--output raw is not supported with saved-work operations", + suggestion: "remove --output raw so azd can manage saved operation state", + }, + { + name: "timeout", + args: []string{"--protocol", "invocations", "--resume", "--timeout", "1"}, + message: "--timeout is not supported with --resume, --steer, or --cancel", + suggestion: "remove --timeout; saved-work operations manage request timing internally", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + cmd := newInvokeCommand(tt.extCtx) + cmd.SetArgs(tt.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := cmd.Execute() + localErr, ok := errors.AsType[*azdext.LocalError](err) + require.True(t, ok) + assert.Equal(t, tt.message, localErr.Message) + assert.Equal(t, tt.suggestion, localErr.Suggestion) + }) + } +} + func TestInvokeCommandBackgroundEndpointRoutesHostFailure(t *testing.T) { isolateFromAzdDaemon(t) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_resume.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_resume.go new file mode 100644 index 00000000000..054a7770b7b --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_resume.go @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "azureaiagent/internal/exterrors" +) + +func (a *InvokeAction) invocationsResumeRemote(ctx context.Context) error { + rc, err := a.resolveRemoteContext(ctx) + if err != nil { + return err + } + if a.endpoint != nil { + if rc.azdClient != nil { + rc.azdClient.Close() + } + return exterrors.Validation( + exterrors.CodeInvalidParameter, + "Invocations --resume is not supported with --agent-endpoint", + "run from an azd project so the saved Invocation can be resolved", + ) + } + if rc.azdClient == nil || rc.agentKey == "" { + if rc.azdClient != nil { + rc.azdClient.Close() + } + return fmt.Errorf("Invocations --resume requires project-backed local state") + } + defer rc.azdClient.Close() + + store := newInvocationStateStore(rc.azdClient) + record, err := store.Get(ctx, rc.agentKey) + if err != nil { + return err + } + if record == nil || record.InvocationID == "" { + return fmt.Errorf( + "no saved invocation found; run `azd ai agent invoke --protocol invocations \"\"` first", + ) + } + token, err := a.acquireBearerToken(ctx) + if err != nil { + return err + } + retrievalURL := buildInvocationRetrievalURL( + rc.projectEndpoint, + rc.name, + record.InvocationID, + record.APIVersion, + record.SessionID, + ) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, retrievalURL, nil) + if err != nil { + return fmt.Errorf("create invocation retrieval request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+token) + applyCustomHeaders(req, a.clientHeaders) + applyRemoteUserIdentityHeader(req, &a.flags.userIdentityFlags) + + resp, err := (&http.Client{Timeout: a.httpTimeout()}).Do(req) //nolint:gosec // validated Foundry endpoint + if err != nil { + return fmt.Errorf("GET %s failed: %w", retrievalURL, err) + } + defer resp.Body.Close() + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf( + "GET %s failed with HTTP %d: %s\n%s\n"+ + "the agent might not support retrieval, the invocation might not be registered yet or might have expired", + retrievalURL, + resp.StatusCode, + resp.Status, + body, + ) + } + + fmt.Printf("Agent: %s (remote, invocations protocol)\n", rc.name) + fmt.Printf("Invocation: %s\n\n", record.InvocationID) + _, err = io.Copy(os.Stdout, resp.Body) + if err != nil { + return fmt.Errorf("write invocation retrieval response: %w", err) + } + return nil +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_store.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_store.go new file mode 100644 index 00000000000..6ee9f386b9c --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_store.go @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "context" + "fmt" + + "github.com/azure/azure-dev/cli/azd/pkg/azdext" +) + +const invocationsConfigPath = configPathPrefix + ".invocations" + +type savedInvocation struct { + InvocationID string `json:"invocationId"` + SessionID string `json:"sessionId,omitempty"` + APIVersion string `json:"apiVersion,omitempty"` +} + +type invocationStateStore struct { + client *azdext.AzdClient +} + +func newInvocationStateStore(client *azdext.AzdClient) *invocationStateStore { + return &invocationStateStore{client: client} +} + +func (s *invocationStateStore) Get(ctx context.Context, agentKey string) (*savedInvocation, error) { + config, err := azdext.NewConfigHelper(s.client) + if err != nil { + return nil, fmt.Errorf("create invocation config helper: %w", err) + } + var records map[string]savedInvocation + found, err := config.GetUserJSON(ctx, invocationsConfigPath, &records) + if err != nil { + return nil, fmt.Errorf("read saved invocations: %w", err) + } + if !found || records == nil { + return nil, nil + } + record, ok := records[agentKey] + if !ok { + return nil, nil + } + return &record, nil +} + +func (s *invocationStateStore) Save(ctx context.Context, agentKey string, record savedInvocation) error { + config, err := azdext.NewConfigHelper(s.client) + if err != nil { + return fmt.Errorf("create invocation config helper: %w", err) + } + var records map[string]savedInvocation + found, err := config.GetUserJSON(ctx, invocationsConfigPath, &records) + if err != nil { + return fmt.Errorf("read saved invocations: %w", err) + } + if !found || records == nil { + records = make(map[string]savedInvocation) + } + records[agentKey] = record + if err := config.SetUserJSON(ctx, invocationsConfigPath, records); err != nil { + return fmt.Errorf("write saved invocations: %w", err) + } + return nil +} + +func (s *invocationStateStore) Delete(ctx context.Context, agentKey string) error { + config, err := azdext.NewConfigHelper(s.client) + if err != nil { + return fmt.Errorf("create invocation config helper: %w", err) + } + var records map[string]savedInvocation + found, err := config.GetUserJSON(ctx, invocationsConfigPath, &records) + if err != nil { + return fmt.Errorf("read saved invocations: %w", err) + } + if !found || records == nil { + return nil + } + delete(records, agentKey) + if err := config.SetUserJSON(ctx, invocationsConfigPath, records); err != nil { + return fmt.Errorf("write saved invocations: %w", err) + } + return nil +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_store_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_store_test.go new file mode 100644 index 00000000000..09f665189b8 --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_invocation_store_test.go @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInvocationStateStoreRoundTrip(t *testing.T) { + server := newInvokeUserConfigServer() + client := newInvokeTestAzdClient(t, server) + store := newInvocationStateStore(client) + want := savedInvocation{ + InvocationID: "inv_123", + SessionID: "sess_123", + APIVersion: "v1", + } + + require.NoError(t, store.Save(t.Context(), "agent", want)) + got, err := store.Get(t.Context(), "agent") + require.NoError(t, err) + require.NotNil(t, got) + assert.Equal(t, want, *got) + + require.NoError(t, store.Delete(t.Context(), "agent")) + got, err = store.Get(t.Context(), "agent") + require.NoError(t, err) + assert.Nil(t, got) +} + +func TestBuildInvocationRetrievalURL(t *testing.T) { + t.Parallel() + + got := buildInvocationRetrievalURL( + "https://acct.services.ai.azure.com/api/projects/proj", + "agent", + "inv/a b", + "v1", + "session/a", + ) + assert.Contains(t, got, "/invocations/inv%2Fa%20b?") + assert.Contains(t, got, "api-version=v1") + assert.Contains(t, got, "agent_session_id=session%2Fa") +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_response_store_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_response_store_test.go index 18fbaf279f8..db0aa887831 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_response_store_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_response_store_test.go @@ -80,6 +80,10 @@ func TestCleanupAgentStateForKey(t *testing.T) { agentKey: {ResponseID: "resp_123"}, otherKey: {ResponseID: "resp_other"}, }) + server.setJSON(t, invocationsConfigPath, map[string]savedInvocation{ + agentKey: {InvocationID: "inv_123"}, + otherKey: {InvocationID: "inv_other"}, + }) client := newInvokeTestAzdClient(t, server) require.True(t, cleanupAgentStateForKey(t.Context(), client, agentKey)) @@ -98,4 +102,9 @@ func TestCleanupAgentStateForKey(t *testing.T) { server.getJSON(t, backgroundResponsesConfigPath, &responses) assert.NotContains(t, responses, agentKey) assert.Equal(t, "resp_other", responses[otherKey].ResponseID) + + var invocations map[string]savedInvocation + server.getJSON(t, invocationsConfigPath, &invocations) + assert.NotContains(t, invocations, agentKey) + assert.Equal(t, "inv_other", invocations[otherKey].InvocationID) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go index b9cae680f2f..558f4066514 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go @@ -535,8 +535,8 @@ func warnLegacySimpleTeamsArtifacts(proj *azdext.ProjectConfig, svc *azdext.Serv )) } -// postdownHandler cleans up saved session, conversation, and background Response state for agent services -// that were torn down. This is best-effort — failures are logged but do not block azd down. +// postdownHandler cleans up saved session, conversation, background Response, and Invocation state for agent +// services that were torn down. This is best-effort — failures are logged but do not block azd down. func postdownHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azdext.ProjectEventArgs) error { envResp, err := azdClient.Environment().GetCurrent(ctx, &azdext.EmptyRequest{}) if err != nil { @@ -552,7 +552,10 @@ func postdownHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azd } if cleanupAgentState(ctx, azdClient, envName, svc.Name) { - fmt.Printf("Cleaned up saved session, conversation, and background Response for agent %q\n", svc.Name) + fmt.Printf( + "Cleaned up saved session, conversation, background Response, and Invocation for agent %q\n", + svc.Name, + ) } } @@ -563,9 +566,8 @@ func postdownHandler(ctx context.Context, azdClient *azdext.AzdClient, args *azd return nil } -// cleanupAgentState removes saved session, conversation, and background Response state for a -// single agent service. Returns true if cleanup succeeded, false otherwise. -// Shared by postdownHandler and delete command. +// cleanupAgentState removes saved session, conversation, background Response, and Invocation state for a single +// agent service. Returns true if cleanup succeeded, false otherwise. Shared by postdownHandler and delete command. func cleanupAgentState(ctx context.Context, azdClient *azdext.AzdClient, envName, serviceName string) bool { serviceKey := toServiceKey(serviceName) @@ -595,6 +597,10 @@ func cleanupAgentStateForKey(ctx context.Context, azdClient *azdext.AzdClient, a log.Printf("cleanupAgentState: failed to clean background Response for %s: %v", agentKey, err) failed = true } + if err := newInvocationStateStore(azdClient).Delete(ctx, agentKey); err != nil { + log.Printf("cleanupAgentState: failed to clean Invocation for %s: %v", agentKey, err) + failed = true + } return !failed }