Skip to content

Commit 63a8f82

Browse files
authored
refactor: agent provider simplification (#1286)
* docs: add spec for agent provider * refactor(agent): remove BaseAgentProvider layer and simplify provider hierarchy
1 parent 70f5131 commit 63a8f82

11 files changed

Lines changed: 239 additions & 109 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Plan: Agent Provider Simplification (ACP-only)
2+
3+
## Summary
4+
5+
Replace the “agent provider” abstraction and detection logic with a single explicit rule: **ACP is the only agent provider and is identified by `providerId === 'acp'`.**
6+
7+
## Current Call Flow (relevant parts)
8+
9+
- Main:
10+
- `ProviderInstanceManager.createProviderInstance()` already special-cases `provider.id === 'acp'`.
11+
- `ProviderInstanceManager.isAgentProvider()` uses `instanceof BaseAgentProvider` and (if instance not created) a constructor prototype check (`isAgentConstructor`).
12+
- `LLMProviderPresenter.isAgentProvider()` exposes this to the renderer via `ILlmProviderPresenter`.
13+
- Renderer:
14+
- `src/renderer/src/stores/modelStore.ts` calls `llmproviderPresenter.isAgentProvider(providerId)` over IPC to choose between:
15+
- `agentModelStore.refreshAgentModels(providerId)` (ACP path)
16+
- `refreshStandardModels + refreshCustomModels` (standard path)
17+
- Other renderer logic already treats ACP as special via `provider.id === 'acp'`.
18+
19+
## Proposed Changes
20+
21+
### 1) Remove agent-provider classification API
22+
23+
- Remove `isAgentProvider(providerId: string)` from:
24+
- `src/shared/types/presenters/llmprovider.presenter.d.ts`
25+
- `src/shared/types/presenters/legacy.presenters.d.ts`
26+
- `src/main/presenter/llmProviderPresenter/index.ts`
27+
- `src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts`
28+
29+
Rationale: It is only used by the renderer for ACP gating, and ACP can be identified locally by ID.
30+
31+
### 2) Replace renderer gating with an explicit ACP check
32+
33+
- In `src/renderer/src/stores/modelStore.ts`:
34+
- Remove the async IPC call `llmP.isAgentProvider(providerId)`.
35+
- Replace with a local predicate: `providerId === 'acp'`.
36+
- Keep the existing ACP refresh path using `agentModelStore.refreshAgentModels('acp')` (no behavioral change).
37+
38+
### 3) Remove `BaseAgentProvider` (optional but preferred)
39+
40+
Because `BaseAgentProvider` is only used by `AcpProvider`, delete the base class and:
41+
42+
- Make `AcpProvider` extend `BaseLLMProvider` directly.
43+
- Move `cleanup()` logic into `AcpProvider` (or delegate to `AcpSessionManager` / `AcpProcessManager`).
44+
- Ensure `cleanup()` is safe to call multiple times and during shutdown.
45+
46+
Notes:
47+
- `acpCleanupHook` currently awaits `cleanup()` even though `BaseAgentProvider.cleanup()` is `void`. Consider standardizing ACP cleanup to `Promise<void>` to match usage.
48+
49+
## Compatibility / Migration
50+
51+
- No user data migration.
52+
- Provider ID `acp` remains unchanged and is treated as a stable internal contract.
53+
- Any internal IPC typing generation must be updated to reflect removal of `isAgentProvider`.
54+
55+
## Test Strategy
56+
57+
Add minimal tests focusing on the only behavioral dependency (renderer model refresh selection):
58+
59+
- Renderer unit test for `modelStore.refreshProviderModels()`:
60+
- When `providerId === 'acp'`, it uses `agentModelStore.refreshAgentModels`.
61+
- When `providerId !== 'acp'`, it uses standard refresh path.
62+
63+
Main-process unit tests are optional; the change is mostly removal and ACP-id checks.
64+
65+
## Rollout
66+
67+
Single PR is acceptable if changes stay localized (types + modelStore + ACP provider base class cleanup).
68+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Agent Provider Simplification (ACP-only)
2+
3+
## Background
4+
5+
DeepChat currently distinguishes between:
6+
7+
- **LLM providers**: network-backed providers that implement `BaseLLMProvider` (OpenAI/Anthropic/etc).
8+
- **Agent providers**: providers that manage local agent sessions/processes (currently only `acp` via `AcpProvider`).
9+
10+
The codebase implements this distinction via a dedicated base class (`BaseAgentProvider`) and a runtime/type-detection API (`isAgentProvider`), which is then consumed from the renderer via IPC.
11+
12+
## Problem
13+
14+
- `BaseAgentProvider` is only used by `AcpProvider`, so the abstraction adds indirection without real reuse.
15+
- Provider type detection is over-engineered (`isAgentConstructor` + prototype checks) and duplicates existing ACP-specific branching.
16+
- The renderer calls `llmproviderPresenter.isAgentProvider(providerId)` over IPC, but the only “agent provider” is `providerId === 'acp'`. This creates unnecessary main↔renderer coupling and call complexity.
17+
18+
## Goals
19+
20+
- Treat **ACP as the only agent provider** and identify it **only by `providerId === 'acp'`**.
21+
- Remove the generic “agent provider type detection” path and the renderer IPC dependency for this decision.
22+
- Keep user-visible behavior unchanged:
23+
- ACP agents still appear as selectable models when ACP is enabled.
24+
- Non-ACP providers keep the standard model/custom-model refresh behavior.
25+
- Shutdown and provider disable still clean up ACP resources.
26+
27+
## Non-goals
28+
29+
- Supporting multiple agent providers beyond ACP.
30+
- Redesigning ACP model derivation (agents-as-models) or session/workspace semantics.
31+
- Changing persisted provider IDs or stored settings schemas.
32+
33+
## Acceptance Criteria
34+
35+
- Renderer no longer calls `llmproviderPresenter.isAgentProvider(...)`; ACP decision is local (`providerId === 'acp'`).
36+
- Main process no longer needs `isAgentConstructor` / prototype-based provider classification.
37+
- No remaining runtime dependency on `BaseAgentProvider` for correctness (ACP cleanup remains correct).
38+
- `pnpm run typecheck`, `pnpm test`, `pnpm run lint` pass.
39+
40+
## Open Questions
41+
42+
- None.
43+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Tasks: Agent Provider Simplification (ACP-only)
2+
3+
1. Update renderer to stop using IPC for agent-provider detection
4+
- Remove `llmproviderPresenter.isAgentProvider` usage from `src/renderer/src/stores/modelStore.ts`.
5+
- Gate ACP behavior by `providerId === 'acp'`.
6+
7+
2. Remove `isAgentProvider` from the presenter contract
8+
- Remove from `src/shared/types/presenters/llmprovider.presenter.d.ts`.
9+
- Remove from `src/shared/types/presenters/legacy.presenters.d.ts`.
10+
- Remove implementation from `src/main/presenter/llmProviderPresenter/index.ts`.
11+
12+
3. Remove main-side agent-provider classification implementation
13+
- Delete `ProviderInstanceManager.isAgentProvider()` and `isAgentConstructor()` in `src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts`.
14+
- Ensure no other code path depends on `BaseAgentProvider` type checks.
15+
16+
4. Remove `BaseAgentProvider` abstraction (preferred)
17+
- Delete `src/main/presenter/llmProviderPresenter/baseAgentProvider.ts`.
18+
- Update `src/main/presenter/llmProviderPresenter/providers/acpProvider.ts` to extend `BaseLLMProvider` directly.
19+
- Keep/adjust ACP cleanup semantics (safe shutdown, provider disable, app quit).
20+
21+
5. Add/adjust tests
22+
- Add a Vitest suite under `test/renderer/**` validating model refresh selection for ACP vs non-ACP.
23+
24+
6. Quality gates
25+
- Run `pnpm run format`, `pnpm run lint`, `pnpm run typecheck`, and `pnpm test`.
26+

src/main/presenter/llmProviderPresenter/baseAgentProvider.ts

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/main/presenter/llmProviderPresenter/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,6 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
118118
return this.providerInstanceManager.getProviderById(id)
119119
}
120120

121-
isAgentProvider(providerId: string): boolean {
122-
return this.providerInstanceManager.isAgentProvider(providerId)
123-
}
124-
125121
async setCurrentProvider(providerId: string): Promise<void> {
126122
// 如果有正在生成的流,先停止它们
127123
await this.stopAllStreams()

src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ProviderBatchUpdate, ProviderChange } from '@shared/provider-operations'
22
import { IConfigPresenter, LLM_PROVIDER } from '@shared/presenter'
33
import { BaseLLMProvider } from '../baseProvider'
4-
import { BaseAgentProvider } from '../baseAgentProvider'
54
import { OpenAIProvider } from '../providers/openAIProvider'
65
import { DeepseekProvider } from '../providers/deepseekProvider'
76
import { SiliconcloudProvider } from '../providers/siliconcloudProvider'
@@ -134,11 +133,6 @@ export class ProviderInstanceManager {
134133
])
135134
}
136135

137-
private static isAgentConstructor(ctor?: ProviderConstructor): boolean {
138-
if (!ctor) return false
139-
return BaseAgentProvider.prototype.isPrototypeOf(ctor.prototype)
140-
}
141-
142136
init(): void {
143137
const providers = this.options.configPresenter.getProviders()
144138
for (const provider of providers) {
@@ -250,24 +244,6 @@ export class ProviderInstanceManager {
250244
return instance
251245
}
252246

253-
isAgentProvider(providerId: string): boolean {
254-
const instance = this.providerInstances.get(providerId)
255-
if (instance) {
256-
return instance instanceof BaseAgentProvider
257-
}
258-
259-
const provider = this.providers.get(providerId)
260-
if (!provider) {
261-
return false
262-
}
263-
264-
const ProviderClass =
265-
ProviderInstanceManager.PROVIDER_ID_MAP.get(provider.id) ??
266-
ProviderInstanceManager.PROVIDER_TYPE_MAP.get(provider.apiType)
267-
268-
return ProviderInstanceManager.isAgentConstructor(ProviderClass)
269-
}
270-
271247
private handleProviderAdd(change: ProviderChange): void {
272248
if (!change.provider) return
273249

src/main/presenter/llmProviderPresenter/providers/acpProvider.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type * as schema from '@agentclientprotocol/sdk/dist/schema.js'
2-
import { SUMMARY_TITLES_PROMPT } from '../baseProvider'
3-
import { BaseAgentProvider } from '../baseAgentProvider'
2+
import { BaseLLMProvider, SUMMARY_TITLES_PROMPT } from '../baseProvider'
43
import type {
54
ChatMessage,
65
LLMResponse,
@@ -58,12 +57,7 @@ type PendingPermissionState = {
5857
reject: (error: Error) => void
5958
}
6059

61-
export class AcpProvider extends BaseAgentProvider<
62-
AcpSessionManager,
63-
AcpProcessManager,
64-
schema.RequestPermissionRequest,
65-
schema.RequestPermissionResponse
66-
> {
60+
export class AcpProvider extends BaseLLMProvider {
6761
private readonly processManager: AcpProcessManager
6862
private readonly sessionManager: AcpSessionManager
6963
private readonly sessionPersistence: AcpSessionPersistence
@@ -102,21 +96,6 @@ export class AcpProvider extends BaseAgentProvider<
10296
void this.initWhenEnabled()
10397
}
10498

105-
protected getSessionManager(): AcpSessionManager {
106-
return this.sessionManager
107-
}
108-
109-
protected getProcessManager(): AcpProcessManager {
110-
return this.processManager
111-
}
112-
113-
protected async requestPermission(
114-
params: schema.RequestPermissionRequest
115-
): Promise<schema.RequestPermissionResponse> {
116-
void params
117-
return { outcome: { outcome: 'cancelled' } }
118-
}
119-
12099
protected async fetchProviderModels(): Promise<MODEL_META[]> {
121100
try {
122101
const acpEnabled = await this.configPresenter.getAcpEnabled()

src/renderer/src/stores/modelStore.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,6 @@ export const useModelStore = defineStore('model', () => {
3838
const customModelQueries = new Map<string, ModelQueryHandle<MODEL_META[]>>()
3939
const enabledModelQueries = new Map<string, ModelQueryHandle<RENDERER_MODEL_META[]>>()
4040
const queryCache = useQueryCache()
41-
const isAgentProvider = async (providerId: string): Promise<boolean> => {
42-
try {
43-
return Boolean(await llmP.isAgentProvider(providerId))
44-
} catch (error) {
45-
console.warn(`[ModelStore] Failed to determine provider type: ${providerId}`, error)
46-
return false
47-
}
48-
}
4941

5042
const matchesProviderModelsEntry = (
5143
entry: { key: readonly unknown[] },
@@ -436,7 +428,7 @@ export const useModelStore = defineStore('model', () => {
436428
}
437429

438430
const refreshProviderModels = async (providerId: string) => {
439-
if (await isAgentProvider(providerId)) {
431+
if (providerId === 'acp') {
440432
try {
441433
const { rendererModels, modelMetas } = await agentModelStore.refreshAgentModels(providerId)
442434
updateProviderModelsCache(providerId, modelMetas)

src/shared/types/presenters/legacy.presenters.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,6 @@ export interface ILlmProviderPresenter {
946946
setProviders(provider: LLM_PROVIDER[]): void
947947
getProviders(): LLM_PROVIDER[]
948948
getProviderById(id: string): LLM_PROVIDER
949-
isAgentProvider(providerId: string): boolean
950949
getExistingProviderInstance?(providerId: string): unknown
951950
getModelList(providerId: string): Promise<MODEL_META[]>
952951
updateModelStatus(providerId: string, modelId: string, enabled: boolean): Promise<void>

src/shared/types/presenters/llmprovider.presenter.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ export interface ILlmProviderPresenter {
175175
setProviders(provider: LLM_PROVIDER[]): void
176176
getProviders(): LLM_PROVIDER[]
177177
getProviderById(id: string): LLM_PROVIDER
178-
isAgentProvider(providerId: string): boolean
179178
getProviderInstance(providerId: string): unknown
180179
getExistingProviderInstance(providerId: string): unknown
181180
getModelList(providerId: string): Promise<MODEL_META[]>

0 commit comments

Comments
 (0)