Files
goodbuddy/src/main/agent/create-runtime.ts
T
mesalogo 56ebd40bd8 feat: unify execution and add workspace file imports
Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content.

Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation.

Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit.

Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
2026-10-04 20:02:59 +08:00

405 lines
15 KiB
TypeScript

import {
ModelAgentRuntime,
noModelTools,
type ModelRuntimeOptions
} from './model-runtime'
import { ContinueAgentRuntime } from './continue-runtime'
import { OpenCodeRuntime, type OpenCodeRuntimeOptions } from './opencode-runtime'
import type { LocalRuntimeRegistry } from './local-runtime-registry'
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 { 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'
import type { DesktopDiagnosticFailureObserver } from '../desktop-diagnostics'
export type AgentCapabilityContext = {
observeFailure?: DesktopDiagnosticFailureObserver
localRuntimeRegistry?: LocalRuntimeRegistry
skillInstructions?: string
skillPackages?: RuntimeSkillPackage[]
mcpServers?: ResolvedMcpServer[]
continueHostCacheRoot?: string
opencodeSharedCacheRoot?: 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: noModelTools
})
}
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: noModelTools
})
}
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 启动器不可用')
}
const options: DeepSeekHarnessRuntimeOptions = {
defaultWorkspace: capabilities.localRuntimeRegistry ? 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,
createToolProvider: (directory) => new ModelToolProvider(
new LocalWorkspaceAccess(directory), capabilities.mcpServers, undefined,
capabilities.knowledgeGateway, capabilities.webSearchEnabled === true,
{ runtimeTarget: 'deepseek-harness' }, capabilities.launchEnvironmentProvider
)
}
const create = () => new DeepSeekHarnessRuntime(options)
if (!capabilities.localRuntimeRegistry) return create()
const configuration = { ...options, defaultWorkspace: undefined, createToolProvider: undefined }
return capabilities.localRuntimeRegistry.acquire(
`deepseek-harness:${profile.id}`,
{
...configuration,
mcpServers: capabilities.mcpServers,
webSearchEnabled: capabilities.webSearchEnabled === true,
launchEnvironment: capabilities.launchEnvironmentProvider?.()
},
workspace,
create
)
}
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 独立模型连接仅支持文本对话协议,不支持图像生成协议'
)
}
const options: OpenCodeRuntimeOptions = {
observeFailure: capabilities.observeFailure,
baseUrl,
embedded,
binaryPath:
settings?.opencodeBinaryPath ??
process.env.GOODBUDDY_OPENCODE_BINARY?.trim() ??
'',
bundledBinaryPath: capabilities.bundledRuntimePaths?.opencode,
bundledConfigPath:
capabilities.bundledRuntimePaths?.opencodeConfig,
configPath:
settings?.opencodeConfigPath ??
process.env.GOODBUDDY_OPENCODE_CONFIG?.trim() ??
'',
modelProfile: settings?.opencodeModelProfile,
skillInstructions: capabilities.skillInstructions,
skillPackages: capabilities.skillPackages,
sharedCacheRoot: capabilities.opencodeSharedCacheRoot,
defaultWorkspace: capabilities.localRuntimeRegistry ? defaultWorkspace : workspace,
knowledgeGateway: capabilities.knowledgeGateway,
mcpServers: capabilities.mcpServers,
customization: settings?.runtimeCustomization.opencode,
launchEnvironmentProvider:
capabilities.launchEnvironmentProvider
}
const create = () => new OpenCodeRuntime(options)
if (!capabilities.localRuntimeRegistry) return create()
const configuration = { ...options, defaultWorkspace: undefined }
return capabilities.localRuntimeRegistry.acquire(
`opencode:${settings?.opencodeModelProfile?.id ?? 'native'}`,
{
...configuration,
launchEnvironment: capabilities.launchEnvironmentProvider?.()
},
workspace,
create
)
}
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,
ripgrepExecutablePath: capabilities.bundledRuntimePaths?.ripgrep,
launchEnvironmentProvider:
capabilities.launchEnvironmentProvider
})
}
return new UnconfiguredAgentRuntime()
}