Repository navigation
Conversation
|
Hi @ZaidLT, thank you for raising the PR to fix the issue. We have reviewed the code changes and it does resolve the issue. I will escalate the PR to the team to review, thank you for your patience and understanding. |
There was a problem hiding this comment.
Thanks for this, it's a careful port of adk-python's _exec_with_plugin / _handle_agent_error_callback. Dispatch order, the closingEarly handling when afterRunCallback throws, and sealing the aborted invocation before afterRun all match upstream. The runner, plugin, abort, and live suites pass locally.
The one I'd like resolved, or explicitly deferred, before merge is the abort-as-thrown-error parity gap in runWithPluginLifecycle. The extraction in runner.ts is a recommendation, and the rest are nits.
Also, since afterRunCallback now fires on abort and early close (matching Python), plugins that treated it as "run succeeded" will see a behavior change. Please mention it in the release notes. A follow-up could pass an outcome/reason to afterRunCallback, because plugins currently can't tell an early close from a normal finish. Implementing the two new hooks in LoggingPlugin would also be a nice follow-up.
| try { | ||
| yield* execute.call(this); | ||
| completed = true; | ||
| } catch (error) { |
There was a problem hiding this comment.
Parity gap: Python treats a CancelledError raised while ctx.is_aborted (or on caller close) as an early close, so after_run still runs and no run error is reported. Here, if a custom agent, tool or node throws (e.g. an AbortError / signal.reason) after abortSignal fires, it's classified as a failure: onAgentErrorCallback + onRunErrorCallback fire and afterRunCallback is skipped. Model errors avoid this because runAndHandleError turns them into events, but other paths don't.
Could we treat a throw while invocationContext.abortSignal?.aborted as an abort/close here (and correspondingly in BaseAgent), or at least document the decision and add a test for it?
| let failed = false; | ||
| let completed = false; | ||
| try { | ||
| yield* execute.call(this); |
There was a problem hiding this comment.
Most of the runner.ts diff is re-indentation from inlining the bodies as async function* and rebinding with execute.call(this) / this: Runner. Extracting the two bodies into private generator methods (e.g. runAsyncWithPlugins / runLiveWithPlugins) and passing a thunk would shrink the diff, drop the this rebinding, and make the lifecycle wrapper easier to read.
| error: | ||
| error instanceof Error | ||
| ? error | ||
| : new Error('Run execution threw a non-Error value.', { |
There was a problem hiding this comment.
Nit: the non-Error wrapping is duplicated in BaseAgent.handleAgentErrorCallback, and each level creates its own new Error(..., {cause}), so the agent-level and run-level notifications for the same thrown value aren't identity-equal. A small shared helper (ideally one that wraps once) would tidy this up.
| 'error', | ||
| ] as const; | ||
|
|
||
| describe.each(['runAsync', 'runLive'] as const)('issue #1132: %s', (mode) => { |
There was a problem hiding this comment.
This reproduction block mostly overlaps the plugin lifecycle edge cases suite below. Could we keep one of them to keep the file smaller? I'd also avoid issue numbers in test names and describe the behavior instead.
| } | ||
| } | ||
| const h = await setupLifecycle('runAsync', new FailingNode({name: 'node'})); | ||
| await expect(collect(h.stream)).rejects.toThrow(); |
There was a problem hiding this comment.
Nit: please assert the specific error here (as the other cases do) rather than a bare toThrow(). Also check that onRunErrorCallback receives it.
| expect(error?.cause).toBe(originalException); | ||
| }); | ||
|
|
||
| afterEach(() => vi.restoreAllMocks()); |
There was a problem hiding this comment.
Nit: move this afterEach up next to the existing beforeEach at the top of the describe. Also, a few for loops in the new tests have no braces, while the surrounding code uses them.
|
Thanks @kalenkevich for the careful review, and @Varun-S10 for the triage. Cancellation: Deliberately, any The revision also extracts the runner methods, shares error normalization without a cache, consolidates tests, and addresses the assertion and style comments. The behavior note explains that Validation: 34 cancellation regressions fail on the previous revision and pass with this fix. Full local suite: 6,581 passed, 55 skipped, with coverage thresholds, build, types, lint, formatting and docs checks passing. |
Closes #1132.
Plugins retaining per-invocation state can miss completion notifications on abort or early iterator close, and lack agent/run error notifications. This adds
onAgentErrorCallbackandonRunErrorCallback, and runsafterRunCallbackwhen a startedrunAsyncorrunLiveinvocation completes, aborts, or closes early without an execution error, following ADK Python's runner.AbortErrororInvocationAbortedError, including wrapped causes. Unrelated failures remain errors.Errorvalues.afterRunCallbackthrows, it is reported as a run error; during caller close, it is logged without replacing the close.beforeRunCallback; session setup stays outside. Existing abort-history sealing and resource cleanup are preserved.This supplies the core error notifications needed by @adriantr's #1134, whose existing error methods can become overrides.
Behavior note for release:
afterRunCallbacknow runs after abort and early iterator close. Plugins must not interpret it as proof that a run succeeded.Tests: the real-runner reproduction had 11 failures and 3 passing controls on unchanged main; all 14 pass with this fix. The revised tests cover 93 runner lifecycle cases, 3 plugin API/dispatch cases and 35 shared error-helper cases, plus extended abort-history and live-connection regressions. The full local suite, explicitly including the integrations workspace, reports 6,581 passed, 55 skipped. Focused and surrounding suites: 387 passed; compiled-package smoke: 16 scenarios passed. Coverage thresholds, build, types, lint, format, docs, license and secrets checks passed. Platform CI remains pending.