Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ hints get an availability notice with unresolved IDs behind a details disclosure
There are no instructions to customize login consent.

Users search and select services, then explicitly click Bind bot or Save changes.
The optional Skill's backend suggestions use this same inventory boundary.
Skill service suggestions are withdrawn; selecting a Skill does not trigger
service discovery or display a recommendation panel.
Required built-in services retain their existing selection and validation rules.
A successful inventory refresh removes missing, inactive or account-denied
selections from the draft, selected count and next submission. Active services
Expand Down
Original file line number Diff line number Diff line change
@@ -1,62 +1,28 @@
# Channel Skill service suggestions
# Channel Skill service suggestions — withdrawn

Issue: https://github.com/aevatarAI/aevatar/issues/3678

## Backend-owned discovery

The console calls `GET /api/skills/service-recommendations?skillName=...` on
Mainnet. The backend owns Skill resolution, service discovery and evidence.
See the [backend contract](https://github.com/aevatarAI/aevatar/blob/3da66b8813c03a7b1d87616889dd3ff343dcde1a/docs/contracts/skill-service-recommendations.md)
for the response, source contracts, layering and limitations.

The backend is delivered in [PR #3683](https://github.com/aevatarAI/aevatar/pull/3683)
against `feature/integrate`. This console change is delivered separately in
[PR #3679](https://github.com/aevatarAI/aevatar/pull/3679) against
`feat/2026-08-04_workflow-activity-vnext`. The backend endpoint must be merged
and deployed for recommendations to become available.

The browser does not fetch `SKILL.md`, load the catalog for inference, or match
service names against Skill content. Its API adapter validates the selected
Skill identity, evidence enums and exact UserService instance IDs. A missing or
unsupported backend result produces a retryable discovery state, with the
manual picker still usable; there is no local inference fallback.

## User behavior

Binding and editing show suggestions after a Skill is selected. Reasons name the
backend's evidence category without exposing private instructions. Account
instances show personal/organization source, activity and availability.
Active services available to the NyxID account determine which exact IDs are
selectable, including services omitted at login. Account-level organization
restrictions still apply. Login service grants do not constrain a channel
Agent Key selection. Both Bind and Edit use this same inventory boundary; see
[Channel service selection](2026-09-28-channel-service-access.md).

Selecting an instance adds its exact UserService ID to the form; normal save
commits it. Discovery never selects, authorizes or removes services. Changing or
clearing the Skill preserves manual choices. Queries are keyed by scope and
Skill name and consume abort signals, so late results do not replace the current
Skill's recommendations.

Missing connections and inactive services offer NyxID connection management in a
new tab. Channel forms no longer show an OAuth service-access review action or
consent instructions. Refresh reloads backend discovery and account inventory.
Pending and failed discovery stay within the suggestion region.

The current backend sources do not declare mandatory dependencies. Suggestions
remain advisory; text mentions explicitly say they may be needed for some tasks.
An empty result does not prove no services are required. Existing platform-
required services keep their own behavior. Inventory is not credential-validity
evidence.

## Design and verification

Retain the compact Channels form, design tokens, wrapped names, keyboard actions
and English/Chinese locales. Excalidraw assets remain unchanged:
`30e74d7b410ae72c4c91432355436679033679c54c10b1702908435b001577de`.
Use real authenticated backend responses; mocks exist only in tests.

Route integration tests cover rendering backend suggestions, exact instance
selection and save, access gaps, switching/clearing, stale responses and recovery.
API tests cover request encoding and malformed/mismatched results.
Full frontend typecheck, suite and production build are delegated to GitHub CI.
The service recommendation feature introduced by [PR #3679](https://github.com/aevatarAI/aevatar/pull/3679)
is withdrawn from Bind and Edit at the user’s request on 2026-09-29.
The backend [PR #3683](https://github.com/aevatarAI/aevatar/pull/3683), targeting
`feature/integrate`, has not been merged or deployed with the frontend.

The console does not request `/api/skills/service-recommendations` or show the
Suggested for panel, Refresh suggestions action, discovery failure message,
or Manage connections in NyxID link. The recommendation component, adapter,
query key, styles and locale messages are removed.

Manual service selection remains governed by
[Channel service selection](2026-09-28-channel-service-access.md): active,
account-available NyxID services can be selected even if omitted at login.
Skill selection, required built-in services, explicit Bind/Save and observed
completion retain their existing behavior.

Any future reintroduction requires explicit approval and verification that the
backend capability is deployed. Backend PR approval or deployment alone must
not silently re-enable this frontend feature.

Existing Bind and Edit route tests verify that a selected Skill produces no
recommendation panel or request. Related channel and inventory tests protect
manual selection and save behavior. Full frontend suite, typecheck and build
remain delegated to GitHub CI.
29 changes: 0 additions & 29 deletions apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,4 @@
export default {
'channels.suggestions.title': 'Suggested for {skill}',
'channels.suggestions.refresh': 'Refresh suggestions',
'channels.suggestions.loading': 'Finding related services...',
'channels.suggestions.error':
'Could not identify related services. Refresh to retry, or select services manually below.',
'channels.suggestions.help':
'Suggestions may be incomplete and do not confirm required dependencies. Select the services your task needs; each addition is saved with the channel.',
'channels.suggestions.linked': 'Linked to this skill in Ornn.',
'channels.suggestions.catalog': 'The service catalog recommends this skill.',
'channels.suggestions.mention':
'Mentioned in the skill description or instructions; may be needed for some tasks.',
'channels.suggestions.unknownSource': 'Unknown source',
'channels.suggestions.checking': 'Checking access...',
'channels.suggestions.accessError':
'Could not check access. Refresh to retry.',
'channels.suggestions.inactive': 'Inactive — manage this service in NyxID.',
'channels.suggestions.unavailable':
'Access unavailable — check with the service owner.',
'channels.suggestions.missingInstance':
'No active connection found. Refresh or check this service in NyxID.',
'channels.suggestions.notSelected': 'Not selected',
'channels.suggestions.selected': 'Selected',
'channels.suggestions.selectNamed': 'Select {service}',
'channels.suggestions.select': 'Select',
'channels.suggestions.notConnected':
'No connection found in your account. Add this service in NyxID.',
'channels.suggestions.empty':
'No related services identified. You can still select services manually below.',
'channels.suggestions.manage': 'Manage connections in NyxID ↗',
'channels.column.owner': 'Owner',
'channels.owner.organization': 'Organization',
'channels.owner.id': 'Owner ID',
Expand Down
26 changes: 0 additions & 26 deletions apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,4 @@
export default {
'channels.suggestions.title': '为 {skill} 推荐',
'channels.suggestions.refresh': '刷新推荐',
'channels.suggestions.loading': '正在识别相关服务…',
'channels.suggestions.error':
'暂时无法识别相关服务。请刷新重试,或在下方手动选择。',
'channels.suggestions.help':
'推荐结果可能不完整,也不代表必需依赖。请按任务需要选择服务,保存 Channel 后生效。',
'channels.suggestions.linked': '该 Skill 在 Ornn 中关联了此服务。',
'channels.suggestions.catalog': '服务目录将该 Skill 列为推荐技能。',
'channels.suggestions.mention':
'Skill 的描述或说明提到了此服务,部分任务可能需要。',
'channels.suggestions.unknownSource': '未知来源',
'channels.suggestions.checking': '正在检查访问权限…',
'channels.suggestions.accessError': '暂时无法检查访问权限,请刷新重试。',
'channels.suggestions.inactive': '服务未启用,请前往 NyxID 管理。',
'channels.suggestions.unavailable': '暂无访问权限,请联系服务所有者。',
'channels.suggestions.missingInstance':
'未找到 active 状态的连接,请刷新或在 NyxID 检查该服务。',
'channels.suggestions.notSelected': '尚未选择',
'channels.suggestions.selected': '已选择',
'channels.suggestions.selectNamed': '选择 {service}',
'channels.suggestions.select': '选择',
'channels.suggestions.notConnected':
'账号中未找到此服务的连接,请前往 NyxID 添加。',
'channels.suggestions.empty': '未识别到相关服务。你仍可在下方手动选择。',
'channels.suggestions.manage': '在 NyxID 管理连接 ↗',
'channels.column.owner': '归属',
'channels.owner.organization': '组织',
'channels.owner.id': '归属 ID',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import WorkflowActivityVNextShell from '../WorkflowActivityVNextShell';
import ChannelServiceAccessNotice from './ChannelServiceAccessNotice';
import ChannelServicePicker from './ChannelServicePicker';
import ChannelSkillField from './ChannelSkillField';
import ChannelSkillServices from './ChannelSkillServices';
import { channelConnectionCss } from './connectionStyles';
import {
ChannelBadge,
Expand Down Expand Up @@ -602,21 +601,6 @@ function ConfigurationForm({
retry={() => void services.refetch()}
editing={editing}
replacesDefaults={baseline?.authorizationMode === 'nyxid_default'}
suggestions={
<ChannelSkillServices
scopeId={scopeId}
skillName={skillName ?? ''}
services={availableServices}
selectedIds={serviceIds}
checkingAccess={services.isFetching || services.isPending}
accessFailed={services.isError}
disabled={busy}
onSelect={(id) =>
setServiceIds((ids) => [...new Set([...ids, id])])
}
refreshAccess={() => void services.refetch()}
/>
}
/>
{failure ? (
<p role="alert" className="channels__form-error">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,7 @@ beforeEach(() => {
return response(linkedSkill());
if (String(input).endsWith(`/skills/${secondSkillId}`))
return response(linkedSkill(secondSkillId));
if (String(input).startsWith('/api/skills/service-recommendations?'))
return response({
skillName: new URL(
String(input),
'https://console.test',
).searchParams.get('skillName'),
suggestions: [],
});

if (String(input).endsWith('/user-services'))
return response({
services: [
Expand Down Expand Up @@ -151,6 +144,18 @@ it('resolves the exact ID outside the search results, then explicitly binds its
expect(selectedSkill()).toHaveTextContent('booking-capacity-renamed'),
);
await waitFor(() => expect(bind).toBeEnabled());
expect(screen.queryByText(/^Suggested for /)).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Refresh suggestions' }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('link', { name: 'Manage connections in NyxID ↗' }),
).not.toBeInTheDocument();
expect(
fetchMock.mock.calls.some(([input]) =>
String(input).includes('/api/skills/service-recommendations'),
),
).toBe(false);
expect(writes()).toHaveLength(0);
fireEvent.click(bind);
await screen.findByRole('alert');
Expand Down Expand Up @@ -222,6 +227,18 @@ it('ignores the URL default when editing a saved registration', async () => {
renderWithQueryClient(<WorkflowActivityVNextPage />);
await screen.findByRole('combobox');
expect(selectedSkill()).toHaveTextContent('saved-support');
expect(screen.queryByText(/^Suggested for /)).not.toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Refresh suggestions' }),
).not.toBeInTheDocument();
expect(
screen.queryByRole('link', { name: 'Manage connections in NyxID ↗' }),
).not.toBeInTheDocument();
expect(
fetchMock.mock.calls.some(([input]) =>
String(input).includes('/api/skills/service-recommendations'),
),
).toBe(false);
expect(screen.getByRole('button', { name: 'Save changes' })).toBeDisabled();
expect(detailReads()).toHaveLength(0);
expect(writes()).toHaveLength(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,6 @@ const selectedServiceIds = [
'user-service-ornn',
];
function catalogue(input: RequestInfo | URL, services = serviceCatalogue) {
if (String(input).startsWith('/api/skills/service-recommendations?'))
return response({
skillName: new URL(
String(input),
'https://console.test',
).searchParams.get('skillName'),
suggestions: [],
});
if (String(input).includes('/skill-search'))
return response({
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export default function ChannelServicePicker({
retry,
editing = false,
replacesDefaults = false,
suggestions,
}: {
readonly accessNotice?: React.ReactNode;
readonly suggestedIds?: readonly string[];
Expand All @@ -36,7 +35,6 @@ export default function ChannelServicePicker({
readonly retry: () => void;
readonly editing?: boolean;
readonly replacesDefaults?: boolean;
readonly suggestions?: React.ReactNode;
}) {
const [search, setSearch] = React.useState('');
const term = search.trim().toLocaleLowerCase();
Expand Down Expand Up @@ -72,7 +70,6 @@ export default function ChannelServicePicker({
)}
</p>
{accessNotice}
{suggestions}
{loading ? (
<AevatarContentSkeleton
ariaLabel={t('channels.connect.servicesLoading', 'Loading services')}
Expand Down
Loading
Loading