fix(core): stop spinners for running activities on remote session error/abort

This commit is contained in:
Adam Weidman
2026-05-18 11:11:10 -04:00
parent 0851c02b72
commit 14f14f58f0
2 changed files with 57 additions and 9 deletions
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -354,7 +354,22 @@ describe('RemoteSessionInvocation', () => {
it('should handle abort gracefully', async () => {
const controller = new AbortController();
const { mockSession } = setupMockSession();
const partialProgress: SubagentProgress = {
isSubagentProgress: true,
agentName: 'Test Agent',
state: SubagentState.RUNNING,
result: '',
recentActivity: [
{
id: 'a1',
type: 'thought',
content: 'Thinking...',
status: SubagentState.RUNNING,
},
],
};
const { mockSession } = setupMockSession({ progress: partialProgress });
// When getResult resolves, the signal will already be aborted
mockSession.getResult.mockImplementation(async () => {
@@ -378,7 +393,10 @@ describe('RemoteSessionInvocation', () => {
updateOutput,
});
expect(result.returnDisplay).toMatchObject({ state: 'error' });
expect(result.returnDisplay).toMatchObject({ state: 'cancelled' });
expect(
(result.returnDisplay as SubagentProgress).recentActivity[0].status,
).toBe(SubagentState.CANCELLED);
expect(result.llmContent).toEqual([
{ text: 'Operation cancelled by user' },
]);
@@ -452,9 +470,10 @@ describe('RemoteSessionInvocation', () => {
// Should contain both the partial output and the error
expect(display.result).toContain('Partial work so far');
expect(display.result).toContain('mid-stream error');
// Should preserve partial activity
// Should preserve and update partial activity status to ERROR
expect(display.recentActivity).toHaveLength(1);
expect(display.recentActivity[0].content).toBe('Thinking...');
expect(display.recentActivity[0].status).toBe(SubagentState.ERROR);
});
it('should clean up listeners in finally', async () => {
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
@@ -17,6 +17,7 @@ import {
type RemoteAgentDefinition,
type AgentInputs,
type SubagentProgress,
type SubagentActivityItem,
SubagentState,
getRemoteAgentTargetUrl,
} from './types.js';
@@ -116,6 +117,7 @@ export class RemoteSessionInvocation extends BaseToolInvocation<
async execute(options: ExecuteOptions): Promise<ToolResult> {
const { abortSignal: _signal, updateOutput } = options;
const agentName = this.definition.displayName ?? this.definition.name;
const emptyActivity: SubagentActivityItem[] = [];
// Seed session with prior A2A conversation state
const stateKey = RemoteSessionInvocation.sessionKey(this.definition);
@@ -172,15 +174,19 @@ export class RemoteSessionInvocation extends BaseToolInvocation<
// rejecting. Detect this and surface proper error state.
if (_signal?.aborted) {
const partialProgress = session.getLatestProgress();
const recentActivity = this.stopRunningActivities(
partialProgress?.recentActivity ?? emptyActivity,
SubagentState.CANCELLED,
);
const errorProgress: SubagentProgress = {
isSubagentProgress: true,
agentName,
state: SubagentState.ERROR,
state: SubagentState.CANCELLED,
result:
typeof partialProgress?.result === 'string'
? partialProgress.result
: '',
recentActivity: partialProgress?.recentActivity ?? [],
recentActivity,
};
if (updateOutput) updateOutput(errorProgress);
return {
@@ -207,12 +213,22 @@ export class RemoteSessionInvocation extends BaseToolInvocation<
? `${partialOutput}\n\n${errorMessage}`
: errorMessage;
const isAbort =
(error instanceof Error && error.name === 'AbortError') ||
errorMessage.includes('Aborted');
const status = isAbort ? SubagentState.CANCELLED : SubagentState.ERROR;
const recentActivity = this.stopRunningActivities(
partialProgress?.recentActivity ?? emptyActivity,
status,
);
const errorProgress: SubagentProgress = {
isSubagentProgress: true,
agentName,
state: SubagentState.ERROR,
state: status,
result: fullDisplay,
recentActivity: partialProgress?.recentActivity ?? [],
recentActivity,
};
if (updateOutput) {
@@ -235,6 +251,19 @@ export class RemoteSessionInvocation extends BaseToolInvocation<
}
}
private stopRunningActivities(
activity: SubagentActivityItem[],
status: SubagentState,
): SubagentActivityItem[] {
const result: SubagentActivityItem[] = [];
for (const item of activity) {
result.push(
item.status === SubagentState.RUNNING ? { ...item, status } : item,
);
}
return result;
}
/**
* Formats an execution error into a user-friendly message.
* Recognizes typed A2AAgentError subclasses and falls back to