OpenCode status checks could cold-start throwaway servers, unrelated conversations could block each other, and model, Continue, or remote paths could replay or duplicate work under failure and backlog conditions. Settings could also lose edits or overwrite unrelated model fields. This update makes status checks passive, keeps per-conversation ordering while allowing concurrency, preserves native long-running work with bounded display capture and backpressure, narrows retries to known pre-dispatch failures, and aligns remote Ask permissions without reducing Execute. It also keeps Embedding updates narrow, improves settings and browser guidance, removes the unused legacy Main gateway state, and records the validated Desktop 0.12.3 and Agent 0.11.17 release metadata. Release note: 改善 OpenCode 切换、多会话长任务、模型重试、远程 Ask/Execute 与设置保存可靠性,并完成 Desktop 0.12.3 / Agent 0.11.17 发布准备。
386 lines
13 KiB
TypeScript
386 lines
13 KiB
TypeScript
import {
|
|
ModelAgentRuntime,
|
|
type ModelRuntimeOptions
|
|
} from './model-runtime'
|
|
import { ContinueAgentRuntime } from './continue-runtime'
|
|
import { OpenCodeRuntime } from './opencode-runtime'
|
|
import {
|
|
DeepSeekHarnessRuntime,
|
|
type DeepSeekHarnessRuntimeOptions
|
|
} from './deepseek-harness-runtime'
|
|
import type { AgentRuntime } from './runtime'
|
|
import { UnconfiguredAgentRuntime } from './unconfigured-runtime'
|
|
import type {
|
|
ResolvedModelProfile,
|
|
ResolvedRuntimeSettings
|
|
} from '../runtime-settings-store'
|
|
import {
|
|
defaultRuntimeSettings,
|
|
isDeepSeekHarnessModelProfile,
|
|
isAgentRuntimeModelProtocol
|
|
} from '../../shared/contracts'
|
|
import type {
|
|
ResolvedMcpServer,
|
|
RuntimeSkillPackage
|
|
} from '../capabilities/capability-service'
|
|
import type { BundledRuntimePaths } from './bundled-runtimes'
|
|
import {
|
|
ContinueHostAdapter,
|
|
type ContinueHostLauncher
|
|
} from './continue-host-adapter'
|
|
import type { BrowserToolService } from '../browser/browser-model-tools'
|
|
import type { ModelToolProviderLike } from './model-tool-provider'
|
|
import type { SubagentScheduler } from '../assistant/subagent-scheduler'
|
|
import type { KnowledgeMcpGateway } from './knowledge-mcp-gateway'
|
|
import { ModelToolProvider } from './model-tool-provider'
|
|
import type { ControlledHarnessExtensionPackage } from './deepseek-harness-extension-loader'
|
|
import {
|
|
LocalWorkspaceAccess,
|
|
type WorkspaceAccess
|
|
} from '../workspace'
|
|
import {
|
|
REMOTE_EXECUTION_SPACE_UNAVAILABLE,
|
|
type ExecutionSpaceDescriptor
|
|
} from '../execution-space'
|
|
import type { LaunchEnvironmentProvider } from '../local-tool-environment/launch-environment-provider'
|
|
|
|
const noSubagentTools: ModelToolProviderLike = {
|
|
listTools: async () => [],
|
|
getApproval: () => {
|
|
throw new Error('子专家不允许工具调用')
|
|
},
|
|
callTool: async () => {
|
|
throw new Error('子专家不允许工具调用')
|
|
},
|
|
releaseConversation: async () => undefined,
|
|
dispose: async () => undefined
|
|
}
|
|
|
|
export type AgentCapabilityContext = {
|
|
skillInstructions?: string
|
|
skillPackages?: RuntimeSkillPackage[]
|
|
mcpServers?: ResolvedMcpServer[]
|
|
continueHostCacheRoot?: string
|
|
bundledRuntimePaths?: BundledRuntimePaths
|
|
continueHostLauncher?: ContinueHostLauncher
|
|
deepseekHarnessLauncher?: DeepSeekHarnessRuntimeOptions['launch']
|
|
deepseekHarnessExtensions?: ControlledHarnessExtensionPackage[]
|
|
browserService?: BrowserToolService
|
|
knowledgeGateway?: KnowledgeMcpGateway
|
|
webSearchEnabled?: boolean
|
|
executionSpace?: ExecutionSpaceDescriptor
|
|
workspaceAccess?: WorkspaceAccess
|
|
directModelSubagentScheduler?: SubagentScheduler
|
|
launchEnvironmentProvider?: LaunchEnvironmentProvider
|
|
}
|
|
|
|
function resolveContextCompression(
|
|
settings: ResolvedRuntimeSettings,
|
|
currentProfile: ResolvedModelProfile | undefined
|
|
): ModelRuntimeOptions['contextCompression'] {
|
|
const compression =
|
|
settings.contextCompression ?? defaultRuntimeSettings.contextCompression
|
|
const source = compression.modelSource
|
|
const summaryProfile =
|
|
source.kind === 'profile'
|
|
? settings.modelProfiles.find(
|
|
(profile) =>
|
|
profile.id === source.profileId &&
|
|
isAgentRuntimeModelProtocol(profile.protocol)
|
|
)
|
|
: undefined
|
|
return {
|
|
settings: compression,
|
|
contextWindowTokens: currentProfile?.contextWindowTokens,
|
|
...(summaryProfile
|
|
? {
|
|
summaryModel: {
|
|
apiKey: summaryProfile.apiKey,
|
|
baseUrl: summaryProfile.baseUrl,
|
|
model: summaryProfile.modelName,
|
|
protocol: summaryProfile.protocol as Exclude<
|
|
typeof summaryProfile.protocol,
|
|
'openai-images-generations'
|
|
>,
|
|
authentication: summaryProfile.authentication,
|
|
contextWindowTokens: summaryProfile.contextWindowTokens,
|
|
maximumOutputTokens: summaryProfile.maximumOutputTokens,
|
|
requestHeaders: summaryProfile.requestHeaders,
|
|
requestBody: summaryProfile.requestBody
|
|
}
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
|
|
export function createDefaultModelRuntime(
|
|
defaultWorkspace: string,
|
|
settings: ResolvedRuntimeSettings
|
|
): AgentRuntime {
|
|
if (settings.modelProtocol === 'openai-images-generations') {
|
|
return new UnconfiguredAgentRuntime()
|
|
}
|
|
const currentProfile = settings.modelProfiles.find(
|
|
(profile) => profile.id === settings.defaultModelProfileId
|
|
)
|
|
return new ModelAgentRuntime({
|
|
apiKey: settings.apiKey,
|
|
baseUrl: settings.modelBaseUrl,
|
|
model: settings.modelName,
|
|
protocol: settings.modelProtocol,
|
|
authentication: settings.modelAuthentication,
|
|
requestHeaders: currentProfile?.requestHeaders,
|
|
requestBody: currentProfile?.requestBody,
|
|
supportsImageInput: settings.supportsImageInput,
|
|
maximumOutputTokens: currentProfile?.maximumOutputTokens,
|
|
defaultWorkspace: settings.workspacePath || defaultWorkspace,
|
|
contextCompression: resolveContextCompression(
|
|
settings,
|
|
currentProfile
|
|
),
|
|
toolProvider: noSubagentTools
|
|
})
|
|
}
|
|
|
|
export function createModelProfileRuntime(
|
|
defaultWorkspace: string,
|
|
settings: ResolvedRuntimeSettings,
|
|
profile: ResolvedModelProfile
|
|
): ModelAgentRuntime {
|
|
return new ModelAgentRuntime({
|
|
apiKey: profile.apiKey,
|
|
baseUrl: profile.baseUrl,
|
|
model: profile.modelName,
|
|
protocol: profile.protocol,
|
|
authentication: profile.authentication,
|
|
requestHeaders: profile.requestHeaders,
|
|
requestBody: profile.requestBody,
|
|
supportsImageInput: profile.supportsImageInput,
|
|
maximumOutputTokens: profile.maximumOutputTokens,
|
|
imageGenerationQuality:
|
|
profile.imageGenerationQuality ??
|
|
defaultRuntimeSettings.imageGenerationQuality,
|
|
contextCompression: resolveContextCompression(settings, profile),
|
|
defaultWorkspace: settings.workspacePath || defaultWorkspace,
|
|
toolProvider: noSubagentTools
|
|
})
|
|
}
|
|
|
|
export function createAgentRuntime(
|
|
defaultWorkspace: string,
|
|
settings?: ResolvedRuntimeSettings,
|
|
capabilities: AgentCapabilityContext = {}
|
|
): AgentRuntime {
|
|
const executionSpace = capabilities.executionSpace
|
|
const remote = executionSpace?.kind === 'ssh'
|
|
const baseUrl = (
|
|
settings?.opencodeBaseUrl ||
|
|
process.env.GOODBUDDY_OPENCODE_URL ||
|
|
''
|
|
).trim()
|
|
const embedded = !baseUrl
|
|
const workspace =
|
|
executionSpace?.kind === 'local'
|
|
? executionSpace.rootPath
|
|
: settings?.workspacePath || defaultWorkspace
|
|
const getWorkspaceAccess = (): WorkspaceAccess =>
|
|
capabilities.workspaceAccess ??
|
|
executionSpace?.workspaceAccess ??
|
|
new LocalWorkspaceAccess(workspace)
|
|
const provider = settings?.provider ?? defaultRuntimeSettings.provider
|
|
|
|
if (
|
|
remote &&
|
|
(provider === 'continue' ||
|
|
provider === 'opencode' ||
|
|
provider === 'deepseek-harness' ||
|
|
provider === 'auto')
|
|
) {
|
|
throw new Error(REMOTE_EXECUTION_SPACE_UNAVAILABLE)
|
|
}
|
|
|
|
if (provider === 'deepseek-harness') {
|
|
const profile = settings?.deepseekHarnessModelProfile
|
|
if (!profile || !isDeepSeekHarnessModelProfile(profile)) {
|
|
throw new Error(
|
|
'DeepSeek Harness 需要使用 API Key 的 OpenAI 兼容 Chat Completions 模型连接'
|
|
)
|
|
}
|
|
if (!profile.apiKey) {
|
|
throw new Error('DeepSeek Harness 模型连接未配置 API Key')
|
|
}
|
|
if (!capabilities.deepseekHarnessLauncher) {
|
|
throw new Error('DeepSeek Harness 受控 Host 启动器不可用')
|
|
}
|
|
return new DeepSeekHarnessRuntime({
|
|
defaultWorkspace: workspace,
|
|
baseUrl: profile.baseUrl,
|
|
model: profile.modelName,
|
|
supportsImageInput: profile.supportsImageInput === true,
|
|
requestHeaders: profile.requestHeaders,
|
|
launch: capabilities.deepseekHarnessLauncher,
|
|
credentialRefs: {
|
|
GOODBUDDY_HARNESS_MODEL_API_KEY: profile.apiKey
|
|
},
|
|
skillPackages: capabilities.skillPackages,
|
|
extensionPackages: capabilities.deepseekHarnessExtensions,
|
|
toolProvider: new ModelToolProvider(
|
|
getWorkspaceAccess(),
|
|
capabilities.mcpServers,
|
|
undefined,
|
|
capabilities.knowledgeGateway,
|
|
capabilities.webSearchEnabled === true,
|
|
{},
|
|
capabilities.launchEnvironmentProvider
|
|
)
|
|
})
|
|
}
|
|
|
|
if (provider === 'continue') {
|
|
if (
|
|
settings?.continueModelProfile &&
|
|
!isAgentRuntimeModelProtocol(
|
|
settings.continueModelProfile.protocol
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'Continue 独立模型连接仅支持文本对话协议,不支持图像生成协议'
|
|
)
|
|
}
|
|
return new ContinueAgentRuntime({
|
|
binaryPath:
|
|
settings?.continueBinaryPath ??
|
|
process.env.GOODBUDDY_CONTINUE_BINARY?.trim() ??
|
|
process.env.GOODBUDDY_CONTINUE_COMMAND?.trim() ??
|
|
'',
|
|
bundledBinaryPath: capabilities.bundledRuntimePaths?.continue,
|
|
configPath:
|
|
settings?.continueConfigPath ??
|
|
process.env.GOODBUDDY_CONTINUE_CONFIG?.trim() ??
|
|
'',
|
|
modelProfile: settings?.continueModelProfile,
|
|
skillInstructions: capabilities.skillInstructions,
|
|
skillPackages: capabilities.skillPackages,
|
|
defaultWorkspace: workspace,
|
|
hostCacheRoot:
|
|
capabilities.continueHostCacheRoot ??
|
|
process.env.GOODBUDDY_CONTINUE_HOST_CACHE?.trim() ??
|
|
'',
|
|
launchHost: capabilities.continueHostLauncher,
|
|
createHostAdapter: capabilities.launchEnvironmentProvider
|
|
? (options) =>
|
|
new ContinueHostAdapter({
|
|
...options,
|
|
launchEnvironmentProvider:
|
|
capabilities.launchEnvironmentProvider
|
|
})
|
|
: undefined,
|
|
knowledgeGateway: capabilities.knowledgeGateway,
|
|
mcpServers: capabilities.mcpServers,
|
|
customization: settings?.runtimeCustomization.continue
|
|
})
|
|
}
|
|
|
|
if (provider === 'opencode' || (provider === 'auto' && (baseUrl || embedded))) {
|
|
if (
|
|
settings?.opencodeModelProfile &&
|
|
!isAgentRuntimeModelProtocol(
|
|
settings.opencodeModelProfile.protocol
|
|
)
|
|
) {
|
|
throw new Error(
|
|
'OpenCode 独立模型连接仅支持文本对话协议,不支持图像生成协议'
|
|
)
|
|
}
|
|
return new OpenCodeRuntime({
|
|
baseUrl,
|
|
embedded,
|
|
binaryPath:
|
|
settings?.opencodeBinaryPath ??
|
|
process.env.GOODBUDDY_OPENCODE_BINARY?.trim() ??
|
|
'',
|
|
bundledBinaryPath: capabilities.bundledRuntimePaths?.opencode,
|
|
configPath:
|
|
settings?.opencodeConfigPath ??
|
|
process.env.GOODBUDDY_OPENCODE_CONFIG?.trim() ??
|
|
'',
|
|
modelProfile: settings?.opencodeModelProfile,
|
|
skillInstructions: capabilities.skillInstructions,
|
|
skillPackages: capabilities.skillPackages,
|
|
defaultWorkspace: workspace,
|
|
knowledgeGateway: capabilities.knowledgeGateway,
|
|
mcpServers: capabilities.mcpServers,
|
|
customization: settings?.runtimeCustomization.opencode,
|
|
launchEnvironmentProvider:
|
|
capabilities.launchEnvironmentProvider
|
|
})
|
|
}
|
|
|
|
const defaultModelProfile =
|
|
settings?.modelProfiles.find(
|
|
(profile) => profile.id === settings.defaultModelProfileId
|
|
) ?? settings?.modelProfiles[0]
|
|
const modelApiKey =
|
|
defaultModelProfile?.apiKey ||
|
|
settings?.apiKey ||
|
|
process.env.GOODBUDDY_MODEL_API_KEY?.trim() ||
|
|
process.env.GOODBUDDY_BIGTOKEN_API_KEY?.trim()
|
|
const modelAuthentication =
|
|
defaultModelProfile?.authentication ??
|
|
settings?.modelAuthentication ??
|
|
defaultRuntimeSettings.modelAuthentication
|
|
if (
|
|
provider === 'model' ||
|
|
(provider === 'auto' &&
|
|
(modelAuthentication === 'none' || modelApiKey))
|
|
) {
|
|
return new ModelAgentRuntime({
|
|
apiKey: modelApiKey ?? '',
|
|
baseUrl:
|
|
defaultModelProfile?.baseUrl ||
|
|
settings?.modelBaseUrl ||
|
|
process.env.GOODBUDDY_MODEL_BASE_URL?.trim() ||
|
|
process.env.GOODBUDDY_BIGTOKEN_BASE_URL?.trim() ||
|
|
defaultRuntimeSettings.modelBaseUrl,
|
|
model:
|
|
defaultModelProfile?.modelName ||
|
|
settings?.modelName ||
|
|
process.env.GOODBUDDY_MODEL_NAME?.trim() ||
|
|
process.env.GOODBUDDY_BIGTOKEN_MODEL?.trim() ||
|
|
defaultRuntimeSettings.modelName,
|
|
protocol:
|
|
defaultModelProfile?.protocol ??
|
|
settings?.modelProtocol ??
|
|
defaultRuntimeSettings.modelProtocol,
|
|
authentication: modelAuthentication,
|
|
requestHeaders: defaultModelProfile?.requestHeaders,
|
|
requestBody: defaultModelProfile?.requestBody,
|
|
supportsImageInput:
|
|
defaultModelProfile?.supportsImageInput ??
|
|
settings?.supportsImageInput ??
|
|
defaultRuntimeSettings.supportsImageInput,
|
|
maximumOutputTokens: defaultModelProfile?.maximumOutputTokens,
|
|
imageGenerationQuality:
|
|
defaultModelProfile?.imageGenerationQuality ??
|
|
settings?.imageGenerationQuality ??
|
|
defaultRuntimeSettings.imageGenerationQuality,
|
|
skillInstructions: capabilities.skillInstructions,
|
|
defaultWorkspace: workspace,
|
|
mcpServers: capabilities.mcpServers,
|
|
browserService: capabilities.browserService,
|
|
knowledgeGateway: capabilities.knowledgeGateway,
|
|
webSearchEnabled: capabilities.webSearchEnabled,
|
|
contextCompression: settings
|
|
? resolveContextCompression(settings, defaultModelProfile)
|
|
: undefined,
|
|
workspaceAccess: getWorkspaceAccess(),
|
|
directModelSubagentScheduler:
|
|
capabilities.directModelSubagentScheduler,
|
|
launchEnvironmentProvider:
|
|
capabilities.launchEnvironmentProvider
|
|
})
|
|
}
|
|
|
|
return new UnconfiguredAgentRuntime()
|
|
}
|