Enable & disable agents (#16225)

This commit is contained in:
Sehoon Shon
2026-01-14 19:30:17 -05:00
committed by GitHub
parent 42c26d1e1b
commit 4b2e9f7954
9 changed files with 800 additions and 13 deletions

View File

@@ -0,0 +1,150 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {
SettingScope,
isLoadableSettingScope,
type LoadedSettings,
} from '../config/settings.js';
import type { ModifiedScope } from './skillSettings.js';
import type { AgentOverride } from '@google/gemini-cli-core';
export type AgentActionStatus = 'success' | 'no-op' | 'error';
/**
* Metadata representing the result of an agent settings operation.
*/
export interface AgentActionResult {
status: AgentActionStatus;
agentName: string;
action: 'enable' | 'disable';
/** Scopes where the agent's state was actually changed. */
modifiedScopes: ModifiedScope[];
/** Scopes where the agent was already in the desired state. */
alreadyInStateScopes: ModifiedScope[];
/** Error message if status is 'error'. */
error?: string;
}
/**
* Enables an agent by ensuring it is not disabled in any writable scope (User and Workspace).
* It sets `agents.overrides.<agentName>.disabled` to `false` if it was found to be `true`.
*/
export function enableAgent(
settings: LoadedSettings,
agentName: string,
): AgentActionResult {
const writableScopes = [SettingScope.Workspace, SettingScope.User];
const foundInDisabledScopes: ModifiedScope[] = [];
const alreadyEnabledScopes: ModifiedScope[] = [];
for (const scope of writableScopes) {
if (isLoadableSettingScope(scope)) {
const scopePath = settings.forScope(scope).path;
const agentOverrides = settings.forScope(scope).settings.agents
?.overrides as Record<string, AgentOverride> | undefined;
const isDisabled = agentOverrides?.[agentName]?.disabled === true;
if (isDisabled) {
foundInDisabledScopes.push({ scope, path: scopePath });
} else {
alreadyEnabledScopes.push({ scope, path: scopePath });
}
}
}
if (foundInDisabledScopes.length === 0) {
return {
status: 'no-op',
agentName,
action: 'enable',
modifiedScopes: [],
alreadyInStateScopes: alreadyEnabledScopes,
};
}
const modifiedScopes: ModifiedScope[] = [];
for (const { scope, path } of foundInDisabledScopes) {
if (isLoadableSettingScope(scope)) {
// Explicitly enable it to override any lower-precedence disables, or just clear the disable.
// Setting to false ensures it is enabled.
settings.setValue(scope, `agents.overrides.${agentName}.disabled`, false);
modifiedScopes.push({ scope, path });
}
}
return {
status: 'success',
agentName,
action: 'enable',
modifiedScopes,
alreadyInStateScopes: alreadyEnabledScopes,
};
}
/**
* Disables an agent by setting `agents.overrides.<agentName>.disabled` to `true` in the specified scope.
*/
export function disableAgent(
settings: LoadedSettings,
agentName: string,
scope: SettingScope,
): AgentActionResult {
if (!isLoadableSettingScope(scope)) {
return {
status: 'error',
agentName,
action: 'disable',
modifiedScopes: [],
alreadyInStateScopes: [],
error: `Invalid settings scope: ${scope}`,
};
}
const scopePath = settings.forScope(scope).path;
const agentOverrides = settings.forScope(scope).settings.agents?.overrides as
| Record<string, AgentOverride>
| undefined;
const isDisabled = agentOverrides?.[agentName]?.disabled === true;
if (isDisabled) {
return {
status: 'no-op',
agentName,
action: 'disable',
modifiedScopes: [],
alreadyInStateScopes: [{ scope, path: scopePath }],
};
}
// Check if it's already disabled in the other writable scope
const otherScope =
scope === SettingScope.Workspace
? SettingScope.User
: SettingScope.Workspace;
const alreadyDisabledInOther: ModifiedScope[] = [];
if (isLoadableSettingScope(otherScope)) {
const otherOverrides = settings.forScope(otherScope).settings.agents
?.overrides as Record<string, AgentOverride> | undefined;
if (otherOverrides?.[agentName]?.disabled === true) {
alreadyDisabledInOther.push({
scope: otherScope,
path: settings.forScope(otherScope).path,
});
}
}
settings.setValue(scope, `agents.overrides.${agentName}.disabled`, true);
return {
status: 'success',
agentName,
action: 'disable',
modifiedScopes: [{ scope, path: scopePath }],
alreadyInStateScopes: alreadyDisabledInOther,
};
}

View File

@@ -0,0 +1,150 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
vi.mock('../config/settings.js', () => ({
SettingScope: {
User: 'User',
Workspace: 'Workspace',
System: 'System',
SystemDefaults: 'SystemDefaults',
},
}));
import { renderAgentActionFeedback } from './agentUtils.js';
import { SettingScope } from '../config/settings.js';
import type { AgentActionResult } from './agentSettings.js';
describe('agentUtils', () => {
describe('renderAgentActionFeedback', () => {
const mockFormatScope = (label: string, path: string) =>
`[${label}:${path}]`;
it('should return error message if status is error', () => {
const result: AgentActionResult = {
status: 'error',
agentName: 'my-agent',
action: 'enable',
modifiedScopes: [],
alreadyInStateScopes: [],
error: 'Something went wrong',
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Something went wrong',
);
});
it('should return default error message if status is error and no error message provided', () => {
const result: AgentActionResult = {
status: 'error',
agentName: 'my-agent',
action: 'enable',
modifiedScopes: [],
alreadyInStateScopes: [],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'An error occurred while attempting to enable agent "my-agent".',
);
});
it('should return no-op message for enable', () => {
const result: AgentActionResult = {
status: 'no-op',
agentName: 'my-agent',
action: 'enable',
modifiedScopes: [],
alreadyInStateScopes: [],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" is already enabled.',
);
});
it('should return no-op message for disable', () => {
const result: AgentActionResult = {
status: 'no-op',
agentName: 'my-agent',
action: 'disable',
modifiedScopes: [],
alreadyInStateScopes: [],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" is already disabled.',
);
});
it('should return success message for enable (single scope)', () => {
const result: AgentActionResult = {
status: 'success',
agentName: 'my-agent',
action: 'enable',
modifiedScopes: [
{ scope: SettingScope.User, path: '/path/to/user/settings' },
],
alreadyInStateScopes: [],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" enabled by setting it to enabled in [user:/path/to/user/settings] settings.',
);
});
it('should return success message for enable (two scopes)', () => {
const result: AgentActionResult = {
status: 'success',
agentName: 'my-agent',
action: 'enable',
modifiedScopes: [
{ scope: SettingScope.User, path: '/path/to/user/settings' },
],
alreadyInStateScopes: [
{
scope: SettingScope.Workspace,
path: '/path/to/workspace/settings',
},
],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" enabled by setting it to enabled in [user:/path/to/user/settings] and [project:/path/to/workspace/settings] settings.',
);
});
it('should return success message for disable (single scope)', () => {
const result: AgentActionResult = {
status: 'success',
agentName: 'my-agent',
action: 'disable',
modifiedScopes: [
{ scope: SettingScope.User, path: '/path/to/user/settings' },
],
alreadyInStateScopes: [],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" disabled by setting it to disabled in [user:/path/to/user/settings] settings.',
);
});
it('should return success message for disable (two scopes)', () => {
const result: AgentActionResult = {
status: 'success',
agentName: 'my-agent',
action: 'disable',
modifiedScopes: [
{ scope: SettingScope.User, path: '/path/to/user/settings' },
],
alreadyInStateScopes: [
{
scope: SettingScope.Workspace,
path: '/path/to/workspace/settings',
},
],
};
expect(renderAgentActionFeedback(result, mockFormatScope)).toBe(
'Agent "my-agent" is now disabled in both [user:/path/to/user/settings] and [project:/path/to/workspace/settings] settings.',
);
});
});
});

View File

@@ -0,0 +1,65 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { SettingScope } from '../config/settings.js';
import type { AgentActionResult } from './agentSettings.js';
/**
* Shared logic for building the core agent action message while allowing the
* caller to control how each scope and its path are rendered (e.g., bolding or
* dimming).
*
* This function ONLY returns the description of what happened. It is up to the
* caller to append any interface-specific guidance.
*/
export function renderAgentActionFeedback(
result: AgentActionResult,
formatScope: (label: string, path: string) => string,
): string {
const { agentName, action, status, error } = result;
if (status === 'error') {
return (
error ||
`An error occurred while attempting to ${action} agent "${agentName}".`
);
}
if (status === 'no-op') {
return `Agent "${agentName}" is already ${action === 'enable' ? 'enabled' : 'disabled'}.`;
}
const isEnable = action === 'enable';
const actionVerb = isEnable ? 'enabled' : 'disabled';
const preposition = isEnable
? 'by setting it to enabled in'
: 'by setting it to disabled in';
const formatScopeItem = (s: { scope: SettingScope; path: string }) => {
const label =
s.scope === SettingScope.Workspace ? 'project' : s.scope.toLowerCase();
return formatScope(label, s.path);
};
const totalAffectedScopes = [
...result.modifiedScopes,
...result.alreadyInStateScopes,
];
if (totalAffectedScopes.length === 2) {
const s1 = formatScopeItem(totalAffectedScopes[0]);
const s2 = formatScopeItem(totalAffectedScopes[1]);
if (isEnable) {
return `Agent "${agentName}" ${actionVerb} ${preposition} ${s1} and ${s2} settings.`;
} else {
return `Agent "${agentName}" is now disabled in both ${s1} and ${s2} settings.`;
}
}
const s = formatScopeItem(totalAffectedScopes[0]);
return `Agent "${agentName}" ${actionVerb} ${preposition} ${s} settings.`;
}