feat(ai): resume and cancel background Responses - #9704
feat(ai): resume and cancel background Responses#9704Wei Meng (m5i-work) wants to merge 3 commits into
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). 20 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Wei Meng (m5i-work)
left a comment
There was a problem hiding this comment.
A few correctness issues need addressing before this layer merges.
Wei Meng (m5i-work)
left a comment
There was a problem hiding this comment.
The updates address the prior findings overall; one reconnect-state edge case remains.
8dca151 to
36937a6
Compare
|
Azure Pipelines: Successfully started running 1 pipeline(s). 20 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds the second background Responses lifecycle slice for the Azure AI Agents extension.
Changes:
- Adds
--no-wait, message-free--continue,--cancel, and--agent-name. - Implements cursor-based reconnect, retries, snapshots, cancellation, and state guards.
- Adds SSE lifecycle handling and focused tests.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
invoke.go |
Adds flags, validation, routing, and foreground state handling. |
invoke_background.go |
Implements reconnect, snapshot, cancellation, and persistence logic. |
invoke_background_test.go |
Tests persistence, cancellation, continuation, and validation. |
invoke_responses_stream.go |
Adds resumed-stream and recovery handling. |
invoke_responses_stream_test.go |
Tests reconnect stream edge cases. |
agent_endpoint.go |
Builds follow, snapshot, and cancel URLs. |
agent_endpoint_test.go |
Tests lifecycle URL construction. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
📋 Prioritization NoteThanks for the contribution! The linked issue isn't in the current milestone yet. |
36937a6 to
cd907f6
Compare
cd907f6 to
0368663
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Foreground standalone invocation regresses, and deterministic SSE parser failures are incorrectly retried.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_responses_stream.go:311
scanner.Err()also reports deterministic parser failures such as an SSE line exceeding the configured 4 MiB token limit. Marking every scanner error as a disconnect makes the follower replay the same oversized event five times and then fall back to an unbounded snapshot instead of reporting the size violation immediately. Add the retry sentinel only for transport read failures and return parser-limit errors directly.
if err := scanner.Err(); err != nil {
return errors.Join(errResponsesStreamDisconnected, fmt.Errorf("read Responses stream: %w", err))
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background.go:324
- The reconnect loop is not covered by a test that drives scripted HTTP streams end to end. Add an
httptestthat drops after sequence 3, verifies the next request usesstarting_after=3, replays 2/3/4, and confirms only 4 is rendered and persisted through terminal completion. The current helper-only tests cannot catch wiring regressions across request creation, parsing, persistence, and retries.
attempt, err := a.followBackgroundResponseOnce(ctx, rc, store, record, progressPersister, writer)
if err != nil {
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Balanced
c2b6ef0 to
a1db459
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Snapshot fallbacks can mask failed Responses, and cancellation may persist an unverified terminal state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background.go:744
- The exhausted-reconnect snapshot also converts
failed,incomplete, andcancelledinto successful command completion. This masks the agent failure specifically when the terminal state is learned by the final snapshot. Print the status, then return the same terminal error as the SSE path; onlycompletedshould return nil.
if err := renderResponseSnapshot(writer, rc.name, result); err != nil {
return errors.Join(reconnectErr, err)
}
if isTerminalResponseStatus(updated.Status) {
return printResponseStatus(writer, updated.Status)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background.go:324
- The reconnect loop has no scripted HTTP test: existing tests exercise the counter, delay, and response classifier independently, but never verify that a dropped stream reconnects with the persisted cursor, suppresses replay, and reaches terminal state. Add an
httptest.Servertest aroundfollowBackgroundResponseso the integration between these helpers is covered.
consecutiveFailures := 0
for {
attempt, err := a.followBackgroundResponseOnce(ctx, rc, store, record, progressPersister, writer)
if err != nil {
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go:1529
- Only flag validation currently covers
--no-wait; no test executes this branch to prove the identity event is persisted, its actual status is printed, and later buffered output is not rendered. Add a scripted streaming-response test for the detach path, since changes to callback ordering can otherwise break resumability while all current tests still pass.
if a.flags.noWait && progress.ResponseID != "" {
return errBackgroundNoWait
}
- Files reviewed: 7/7 changed files
- Comments generated: 3
- Review effort level: Balanced
a1db459 to
6ed12b6
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Terminal-state handling can skip buffered output, and the reconnect loop lacks end-to-end regression coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background.go:323
- The added tests exercise retry helpers separately but never execute this reconnect loop. Add a scripted
httptest.Serverregression that accepts progress, drops the stream, verifies the next GET uses the persistedstarting_aftercursor, suppresses replay, and reaches a terminal event; otherwise the PR's central recovery path can regress while all current tests pass.
attempt, err := a.followBackgroundResponseOnce(ctx, rc, store, record, progressPersister, writer)
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Balanced
6ed12b6 to
0b1730d
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Moderate issues remain in foreground compatibility, persistence cleanup, cancellation handling, stream rendering, and --no-wait coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (5)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go:1315
- Ordinary foreground Responses now call this helper too, so this unconditional failure removes the existing standalone
--agent-endpointbehavior:resolveRemoteContextexplicitly proceeds without an azd client (invoke.go:1075-1081), and foreground invocations previously needed no response store. Require state only for--resumable; return a nil store for an ordinary foreground create so this PR does not regress that path.
if rc.azdClient == nil {
// The extension executable was launched directly without a parent azd process.
// UserConfig and active-Response conflict detection are unavailable.
return nil, responseStateUnavailable(nil)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go:1500
- The record is deleted only after SSE parsing succeeds. Once the POST returns 2xx, the foreground turn has already been accepted and may advance or change the conversation/session; a later stream disconnect, decode error, or terminal failure returns above and leaves the previous background Response selected, so
--resumecan target stale work. Clear the terminal record at acceptance and join any cleanup error with the later stream error.
if responseStore != nil {
if err := responseStore.Delete(ctx, agentKey); err != nil {
return fmt.Errorf("clear previous background Response: %w", err)
}
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go:1529
- The new
--no-waitoperation is only covered by flag and persister unit tests; no test executes this callback/early-return path. Add an HTTP-level regression that sends an identity event followed by buffered output and verifies the ID/status/cursor are saved, later output is not rendered, and the command returns successfully.
if a.flags.noWait && progress.ResponseID != "" {
return errBackgroundNoWait
}
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_background.go:599
- A successful response with an empty, malformed, or status-less body is treated as definitively
cancelled. That terminal value makes later--resumeshort-circuit without checking the service, potentially hiding work that is still cancelling or active. Only persist a terminal status returned by the service; otherwise refresh the snapshot or preserve the prior nonterminal status and surface the decode problem.
snapshot, decodeErr := decodeResponseSnapshot(body)
if decodeErr == nil && snapshot.Status != "" {
record.Status = snapshot.Status
} else {
record.Status = "cancelled"
}
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_responses_stream.go:192
- Resetting
printedafter rendering the recovery snapshot lets the terminal branch render the same snapshot again when no text delta arrives in between. A recoveryresponse.in_progressfollowed directly byresponse.completedwill therefore print the checkpoint output twice. Track “snapshot output already rendered” separately from whether a delta line is open, and skip terminal snapshot rendering when that output has already been shown.
printed = false
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Moderate issues remain in persistence, cleanup, reconnect behavior, identity validation, and error classification.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go:1535
- This reconnect condition ignores
flushErrandcloseErr. When the stream disconnects at the same time cursor persistence fails, the command silently discards the persistence failure and resumes from in-memory state that was never saved, weakening later recovery. Reconnect only when both persistence operations succeeded; otherwise return the joined errors.
if streamErr != nil && ctx.Err() == nil && progressPersister.latest.ResponseID != "" &&
isRetryableBackgroundStreamError(streamErr) {
latest := progressPersister.latest
return a.followBackgroundResponse(ctx, rc, responseStore, latest, os.Stdout)
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke.go:1488
- A foreground Response is already accepted once this branch is reached, but the saved background record is cleared only when SSE parsing succeeds. If the accepted turn ends with
response.failedor a later rendering/read error, the conversation has advanced while--resumestill selects the older Response. Clear the record regardless of the stream result, then join any stream and cleanup errors.
if responseStore != nil {
if err := responseStore.Delete(ctx, agentKey); err != nil {
return fmt.Errorf("clear previous background Response: %w", err)
}
cli/azd/extensions/azure.ai.agents/internal/cmd/invoke_responses_stream.go:311
Scanner.Err()is not limited to transport failures; it also reports an oversized token when one SSE line exceeds the configured maximum. Adding the disconnect sentinel to every scanner error makes that deterministic protocol/size failure retry five times and potentially fall through to snapshot recovery. Return the size-limit error as non-retryable and reserve this sentinel for actual response-body I/O failures.
if err := scanner.Err(); err != nil {
return errors.Join(errResponsesStreamDisconnected, fmt.Errorf("read Responses stream: %w", err))
- Files reviewed: 9/9 changed files
- Comments generated: 4
- Review effort level: Balanced
|
Superseded by #9900, which replaces invoke lifecycle flags and durable cursor state with orthogonal |
|
Azure Pipelines: Successfully started running 2 pipeline(s). 20 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
azure.ai.agents PR buildNote This is an unsigned development build. Install it only if you trust this PR. Install the extension: azd ext install "https://azuresdkartifacts.z5.web.core.windows.net/azd/extensions/pr/9704/azure-ai-agents.zip"
|
Summary
Second vertical slice of #9676, stacked on #9703.
--resumable --no-waitto detach after service acknowledgement and message-free--resumeto reconnect to background Responses--cancelwithout stopping the hosted sessionSteering is implemented separately through
--steerin #9705.Validation
go test ./...andgo fix ./...incli/azd/extensions/azure.ai.agentsStack
Fixes #9778