import { toolOperationSummary } from './tool-operation-summary' import { realpath } from 'node:fs/promises' import type { AgentRuntimeStatus, ModelRequestHeaders, RuntimeNativeSnapshot, RuntimeNativeTool } from '../../shared/contracts' import { runtimeNativeInventoryLimits, runtimeNativeSkillSchema, runtimeNativeToolSchema } from '../../shared/runtime-customization-contracts' import { RequestError } from '@agentclientprotocol/sdk' import type { AgentExecutionRequest, AgentRuntime, RuntimeAuthorizer, RuntimeEvent } from './runtime' import type { ModelToolDefinition, ModelToolProviderLike } from './model-tool-provider' import type { RuntimeSkillPackage } from '../capabilities/capability-service' import type { ControlledHarnessExtensionPackage } from './deepseek-harness-extension-loader' import { assertObjectJsonSchema, validateJsonSchemaValue } from '@deepseek-ai/dsh-tools' import { GOODBUDDY_CONTROL_PROTOCOL_VERSION, GOODBUDDY_CREDENTIAL, GOODBUDDY_EVENT, GOODBUDDY_HANDSHAKE, GOODBUDDY_NATIVE_SNAPSHOT, GOODBUDDY_PREPARE, GOODBUDDY_RELEASE, GOODBUDDY_SHUTDOWN, GOODBUDDY_TOOLS_CALL, GOODBUDDY_TOOLS_LIST } from './deepseek-harness-protocol' import { deepSeekHarnessStartupBudget } from './deepseek-harness-control-protocol' import { promptWithUntrustedConversationHistory } from './runtime-conversation-history' const ACP_PACKAGE_NAME = '@agentclientprotocol/sdk' const DEFAULT_INITIALIZATION_TIMEOUT_MS = 10_000 const DEFAULT_SHUTDOWN_TIMEOUT_MS = 2_000 const DEFAULT_MAX_STDERR_BYTES = 64 * 1024 const DEFAULT_MAX_EVENT_CHARACTERS = 64 * 1024 const MAX_QUEUED_UPDATES = 1_000 const MAX_APPROVAL_DETAIL_CHARACTERS = 4_000 const MAX_MCP_PROXY_TOOLS = 100 const MAX_MCP_TOOL_DESCRIPTION_CHARACTERS = 1_000 const MAX_NATIVE_TOOLS = runtimeNativeInventoryLimits.tools const MAX_MCP_TOOL_SCHEMA_BYTES = 32 * 1024 const MAIN_WEB_TOOL_NAMES = new Set(['web_search', 'web_fetch']) const DSH_BUILTIN_TOOL_KINDS: Readonly< Partial> > = { bash: 'shell', edit: 'write', pwsh: 'shell', read: 'read', read_image: 'read', write: 'write' } const DSH_SCHEMA_SCALAR_KEYS = new Set([ 'type', 'required', 'additionalProperties', 'enum', 'const', 'description', 'title', 'default', 'examples' ]) type AcpPermissionRequest = { sessionId: string toolCall: { toolCallId: string title?: string | null name?: string | null kind?: string | null rawInput?: unknown } options: Array<{ optionId: string name: string kind: | 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always' }> } type AcpSessionNotification = { sessionId: string update: { sessionUpdate: string content?: { type: string; text?: string } toolCallId?: string title?: string | null name?: string | null status?: string | null rawInput?: unknown rawOutput?: unknown goodBuddyEvent?: Record } } type AcpAgent = { initialize(params: unknown): Promise newSession(params: unknown): Promise<{ sessionId: string }> prompt(params: unknown): Promise<{ stopReason?: string }> cancel(params: { sessionId: string }): Promise extMethod( method: string, params: Record ): Promise> extNotification( method: string, params: Record ): Promise } type AcpConnection = { readonly signal: AbortSignal readonly closed: Promise } export type DeepSeekHarnessAcpSdk = { PROTOCOL_VERSION: number ClientSideConnection: new ( toClient: (agent: AcpAgent) => { requestPermission( params: AcpPermissionRequest ): Promise sessionUpdate(params: AcpSessionNotification): Promise extMethod( method: string, params: Record ): Promise> extNotification( method: string, params: Record ): Promise }, stream: unknown ) => AcpConnection & AcpAgent ndJsonStream( output: WritableStream, input: ReadableStream ): unknown } /** * Process-neutral child contract. The main process may adapt either a Node * ChildProcess or an Electron UtilityProcess to these WHATWG byte streams. */ export type DeepSeekHarnessChild = { readonly stdin: WritableStream readonly stdout: ReadableStream readonly stderr?: ReadableStream readonly exited: Promise<{ exitCode: number | null signal?: string | null }> terminate(): void | Promise } export type DeepSeekHarnessLaunchOptions = { cwd: string signal: AbortSignal baseUrl: string model: string supportsImageInput: boolean requestHeaders?: ModelRequestHeaders credentialRefs: readonly string[] skillPackages: readonly RuntimeSkillPackage[] extensionPackages: readonly ControlledHarnessExtensionPackage[] } export type DeepSeekHarnessRuntimeOptions = { defaultWorkspace: string baseUrl: string model: string supportsImageInput?: boolean requestHeaders?: ModelRequestHeaders launch: ( options: DeepSeekHarnessLaunchOptions ) => Promise /** * Explicit hard timeout for each initialization operation, including the * complete launcher call. When omitted, launcher startup is expanded from * the enabled extension count while later ACP operations retain 10 seconds. */ initializationTimeoutMs?: number promptTimeoutMs?: number shutdownTimeoutMs?: number maxStderrBytes?: number maxEventCharacters?: number credentialRefs?: Readonly> skillPackages?: RuntimeSkillPackage[] extensionPackages?: ControlledHarnessExtensionPackage[] toolProvider?: ModelToolProviderLike createToolProvider?: (workspace: string) => ModelToolProviderLike loadAcpSdk?: () => Promise } type ActiveRun = { request: AgentExecutionRequest toolProvider?: ModelToolProviderLike toolController: AbortController authorize?: RuntimeAuthorizer updates: AcpSessionNotification['update'][] toolNames: Map< string, { name: string; input?: string; summary?: string; title?: string } > wake?: () => void closed: boolean } type HarnessState = { workspace: string child: DeepSeekHarnessChild connection: AcpConnection agent: AcpAgent capabilities: GoodBuddyHarnessCapabilities } type GoodBuddyHarnessCapabilities = { controlProtocolVersion: number harnessVersion: string acpProtocolVersion: number supports: { cancellation: boolean sessionRelease: boolean reasoningEvents: boolean toolEvents: boolean usageEvents: boolean credentialResolution: boolean } execution: { mode: 'host' } } function safeStringify(value: unknown): string | undefined { if (value === undefined) { return undefined } try { const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2) return text.slice(0, MAX_APPROVAL_DETAIL_CHARACTERS) } catch { return '[无法序列化]' } } export function harnessPromptError(error: unknown): unknown { if (!(error instanceof RequestError) || error.code !== -32603) { return error } const rawDetails = error.data && typeof error.data === 'object' && !Array.isArray(error.data) && typeof (error.data as Record).details === 'string' ? (error.data as Record).details : undefined const details = typeof rawDetails === 'string' ? rawDetails : undefined return details ? new Error(details.slice(0, MAX_APPROVAL_DETAIL_CHARACTERS)) : error } function isMainWebTool( tool: Awaited< ReturnType >[number] ): boolean { return ( tool.source === 'builtin' && MAIN_WEB_TOOL_NAMES.has(tool.name) ) } function dshCompatibleMainInputSchema( schema: Record ): Record { const compatible: Record = {} for (const [key, value] of Object.entries(schema)) { if (DSH_SCHEMA_SCALAR_KEYS.has(key)) { compatible[key] = value continue } if ( key === 'properties' && value && typeof value === 'object' && !Array.isArray(value) ) { compatible.properties = Object.fromEntries( Object.entries(value).map(([name, propertySchema]) => [ name, propertySchema && typeof propertySchema === 'object' && !Array.isArray(propertySchema) ? dshCompatibleMainInputSchema( propertySchema as Record ) : propertySchema ]) ) continue } if ( key === 'items' && value && typeof value === 'object' && !Array.isArray(value) ) { compatible.items = dshCompatibleMainInputSchema( value as Record ) continue } if (key === 'oneOf' && Array.isArray(value)) { compatible.oneOf = value.map((candidate) => candidate && typeof candidate === 'object' && !Array.isArray(candidate) ? dshCompatibleMainInputSchema( candidate as Record ) : candidate ) } } return compatible } function proxyToolInputSchema( tool: Awaited< ReturnType >[number] ): Record { if (isStoryGraphTool(tool.name)) { const schema = structuredClone(tool.inputSchema) const scope = (schema.properties as Record>).scope! // Scope is a discriminated union: Global and projects are mutually exclusive. scope.oneOf = scope.anyOf delete scope.anyOf return dshCompatibleMainInputSchema(schema) } return isMainWebTool(tool) || isMainImageTool(tool.name) ? dshCompatibleMainInputSchema(tool.inputSchema) : tool.inputSchema } function isMainImageTool(name: string): boolean { return name === 'generate_image' || name === 'save_image' } function boundedProxyToolCatalog( tools: Awaited< ReturnType > ): Array<{ name: string description: string inputSchema: Record }> { const catalog = tools.filter( (tool) => isMainWebTool(tool) || isStoryGraphTool(tool.name) || tool.source === 'mcp' || isMainImageTool(tool.name) ) if (catalog.length > MAX_MCP_PROXY_TOOLS) { throw new Error( 'DeepSeek Harness MCP 工具数量超过安全限制' ) } const names = new Set() return catalog.map((tool) => { if ( !/^[a-zA-Z0-9_-]{1,64}$/u.test(tool.name) || names.has(tool.name) ) { throw new Error('DeepSeek Harness MCP 工具名称无效或冲突') } names.add(tool.name) const description = tool.description.slice( 0, tool.name === 'generate_image' ? imageToolDescriptionLimit : MAX_MCP_TOOL_DESCRIPTION_CHARACTERS ) const inputSchema = proxyToolInputSchema(tool) let serialized: string try { serialized = JSON.stringify(inputSchema) } catch (error) { throw new Error('DeepSeek Harness MCP 工具结构无效', { cause: error }) } if ( !serialized || Buffer.byteLength(serialized, 'utf8') > MAX_MCP_TOOL_SCHEMA_BYTES ) { throw new Error( 'DeepSeek Harness MCP 工具结构超过安全限制' ) } return { name: tool.name, description, inputSchema } }) } function timeoutError(label: string): Error { return new Error(`DeepSeek Harness ${label}超时`) } async function withTimeout( operation: Promise, timeoutMs: number, label: string ): Promise { let timer: ReturnType | undefined const timeout = new Promise((_resolve, reject) => { timer = setTimeout(() => reject(timeoutError(label)), timeoutMs) }) try { return await Promise.race([operation, timeout]) } finally { if (timer) { clearTimeout(timer) } } } async function defaultLoadAcpSdk(): Promise { // Keep the process launcher injectable while still using the official ACP // implementation by default. The non-literal import also lets this isolated // runtime land before the dependency wiring change. return (await import( ACP_PACKAGE_NAME )) as unknown as DeepSeekHarnessAcpSdk } export class DeepSeekHarnessRuntime implements AgentRuntime { readonly runtimeId = 'deepseek-harness' readonly requiresToolApproval = false readonly supportsToolExecution = true readonly supportsScopedDataTools = false private state?: HarnessState private initialization?: Promise private launchController?: AbortController private disposed = false private fatalError?: Error private stderrBytes = 0 private readonly sessions = new Map() private readonly workspaceTools = new Map() private readonly proxyToolCatalogs = new Map() private readonly sessionInitializations = new Map< string, Promise >() private readonly activeRuns = new Map() private readonly conversationTails = new Map>() constructor(private readonly options: DeepSeekHarnessRuntimeOptions) {} private get initializationTimeoutMs(): number { return ( this.options.initializationTimeoutMs ?? DEFAULT_INITIALIZATION_TIMEOUT_MS ) } private get launchTimeoutMs(): number { return ( this.options.initializationTimeoutMs ?? deepSeekHarnessStartupBudget( this.options.extensionPackages?.length ?? 0 ).mainTimeoutMs ) } private get promptTimeoutMs(): number | undefined { return this.options.promptTimeoutMs } private get shutdownTimeoutMs(): number { return ( this.options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS ) } private get maxStderrBytes(): number { return this.options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES } private get maxEventCharacters(): number { return ( this.options.maxEventCharacters ?? DEFAULT_MAX_EVENT_CHARACTERS ) } private async terminate( child: DeepSeekHarnessChild ): Promise { try { await child.terminate() } catch { // Termination is best-effort; the bounded exit wait still prevents hangs. } } private fail(error: Error): void { if (this.fatalError) { return } this.fatalError = error for (const run of this.activeRuns.values()) { run.closed = true run.wake?.() run.wake = undefined } } private async consumeStderr( child: DeepSeekHarnessChild ): Promise { if (!child.stderr) { return } const reader = child.stderr.getReader() try { while (true) { const result = await reader.read() if (result.done) { return } this.stderrBytes += result.value.byteLength if (this.stderrBytes > this.maxStderrBytes) { this.fail( new Error( `DeepSeek Harness stderr 超过 ${this.maxStderrBytes} 字节安全限制` ) ) await this.terminate(child) return } } } catch { this.fail(new Error('DeepSeek Harness stderr 管道读取失败')) } finally { reader.releaseLock() } } private handleExit(result: { exitCode: number | null signal?: string | null }): void { if (this.disposed) { return } const suffix = result.signal ? `signal ${result.signal}` : `code ${result.exitCode ?? 'unknown'}` this.fail(new Error(`DeepSeek Harness 进程意外退出(${suffix})`)) } private onSessionUpdate( notification: AcpSessionNotification ): void { const run = this.activeRuns.get(notification.sessionId) if (!run || run.closed) { return } if (run.updates.length >= MAX_QUEUED_UPDATES) { this.fail( new Error('DeepSeek Harness ACP 更新积压超过安全限制') ) return } run.updates.push(notification.update) run.wake?.() run.wake = undefined } private onBridgeEvent(params: Record): void { const sessionId = params.sessionId const requestId = params.requestId if ( typeof sessionId !== 'string' || typeof requestId !== 'string' ) { this.fail(new Error('DeepSeek Harness 扩展事件关联无效')) return } const run = this.activeRuns.get(sessionId) if ( !run || run.closed || run.request.requestId !== requestId ) { return } const eventLength = JSON.stringify(params).length if (eventLength > this.maxEventCharacters) { this.fail( new Error('DeepSeek Harness 扩展事件超过安全限制') ) return } const event = this.bridgeEventToUpdate(params) ?? { sessionUpdate: 'goodbuddy_event', goodBuddyEvent: params } this.onSessionUpdate({ sessionId, update: event }) } private bridgeEventToUpdate( params: Record ): AcpSessionNotification['update'] | undefined { if (params.type === 'text' || params.type === 'reasoning') { if (typeof params.delta !== 'string') { return undefined } return { sessionUpdate: params.type === 'text' ? 'agent_message_chunk' : 'agent_thought_chunk', content: { type: 'text', text: params.delta } } } if (params.type === 'tool') { if ( typeof params.callId !== 'string' || typeof params.name !== 'string' ) { return undefined } const status = params.state === 'running' ? 'in_progress' : params.state === 'completed' ? 'completed' : params.state === 'failed' ? 'failed' : 'pending' return { sessionUpdate: status === 'pending' ? 'tool_call' : 'tool_call_update', toolCallId: params.callId, name: params.name, status, rawInput: params.input, rawOutput: params.output } } return undefined } private toUsageEvent( requestId: string, params: Record ): RuntimeEvent | undefined { if ( params.type !== 'model-usage' || typeof params.callId !== 'string' || typeof params.provider !== 'string' || typeof params.model !== 'string' ) { return undefined } const counts = [ params.inputTokens, params.outputTokens, params.cacheReadTokens, params.cacheWriteTokens ] if ( counts.some( (value) => typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0 ) ) { return undefined } return { requestId, type: 'model-usage', callId: params.callId, runtime: 'deepseek-harness', provider: params.provider, model: params.model, inputTokens: params.inputTokens as number, outputTokens: params.outputTokens as number, cacheReadTokens: params.cacheReadTokens as number, cacheWriteTokens: params.cacheWriteTokens as number } } private parseCapabilities( value: Record, protocolVersion: number ): GoodBuddyHarnessCapabilities { const capabilities = value as unknown as GoodBuddyHarnessCapabilities const supports = capabilities.supports if ( capabilities.controlProtocolVersion !== GOODBUDDY_CONTROL_PROTOCOL_VERSION || capabilities.acpProtocolVersion !== protocolVersion || typeof capabilities.harnessVersion !== 'string' || !supports?.cancellation || !supports.sessionRelease || !supports.credentialResolution || capabilities.execution?.mode !== 'host' ) { throw new Error( 'DeepSeek Harness 内部控制面必需能力握手失败' ) } return capabilities } private async handlePermission( permission: AcpPermissionRequest ): Promise<{ outcome: | { outcome: 'selected'; optionId: string } | { outcome: 'cancelled' } }> { const run = this.activeRuns.get(permission.sessionId) const reject = permission.options.find( (option) => option.kind === 'reject_once' || option.kind === 'reject_always' ) const allowOnce = permission.options.find( (option) => option.kind === 'allow_once' ) ?? permission.options.find((option) => option.kind === 'allow_always') if ( !run || run.closed ) { return reject ? { outcome: { outcome: 'selected', optionId: reject.optionId } } : { outcome: { outcome: 'cancelled' } } } if (allowOnce) { return { outcome: { outcome: 'selected', optionId: allowOnce.optionId } } } return reject ? { outcome: { outcome: 'selected', optionId: reject.optionId } } : { outcome: { outcome: 'cancelled' } } } private async initialize(): Promise { if (this.disposed) { throw new Error('DeepSeek Harness Runtime 已关闭') } const launchController = new AbortController() this.launchController = launchController let child: DeepSeekHarnessChild | undefined try { const workspace = await realpath(this.options.defaultWorkspace) child = await withTimeout( this.options.launch({ cwd: workspace, signal: launchController.signal, baseUrl: this.options.baseUrl, model: this.options.model, supportsImageInput: this.options.supportsImageInput === true, requestHeaders: this.options.requestHeaders ?? {}, credentialRefs: Object.keys( this.options.credentialRefs ?? {} ), skillPackages: this.options.skillPackages ?? [], extensionPackages: this.options.extensionPackages ?? [] }), this.launchTimeoutMs, '启动' ) if (this.disposed) { throw new Error('DeepSeek Harness Runtime 已关闭') } const sdk = await (this.options.loadAcpSdk ?? defaultLoadAcpSdk)() let agent: AcpAgent | undefined const connection = new sdk.ClientSideConnection( (connectedAgent) => { agent = connectedAgent return { requestPermission: (params) => this.handlePermission(params), sessionUpdate: async (params) => { this.onSessionUpdate(params) }, extMethod: async (method, params) => { if (method === GOODBUDDY_CREDENTIAL) { const ref = params.ref if (typeof ref !== 'string') { return {} } const value = this.options.credentialRefs?.[ref] return value ? { value } : {} } if (method === GOODBUDDY_TOOLS_LIST) { if (typeof params.sessionId !== 'string') { throw new Error( 'DeepSeek Harness MCP 工具上下文不可用' ) } const run = this.activeRuns.get(params.sessionId) if (!run?.toolProvider || run.closed) return { tools: [] } const context = { imageToolBinding: run.request.imageToolBinding, conversationId: run?.request.conversationId ?? 'deepseek-harness-tool-catalog', browserTabId: run?.request.browserTabId, knowledgeCapabilityToken: run?.request.knowledgeCapabilityToken } const tools = await run.toolProvider.listTools( context, connection.signal ) const catalog = boundedProxyToolCatalog(tools) this.proxyToolCatalogs.set(params.sessionId, tools.filter( (tool) => tool.source === 'mcp' || isMainWebTool(tool) || isStoryGraphTool(tool.name) || isMainImageTool(tool.name) )) return { tools: catalog } } if (method === GOODBUDDY_TOOLS_CALL) { const sessionId = params.sessionId const name = params.name const argumentsValue = params.arguments const run = typeof sessionId === 'string' ? this.activeRuns.get(sessionId) : undefined if ( !run || run.closed || !run.toolProvider || typeof name !== 'string' || !argumentsValue || typeof argumentsValue !== 'object' || Array.isArray(argumentsValue) ) { throw new Error( 'DeepSeek Harness MCP 工具调用无效' ) } const context = { imageToolBinding: run.request.imageToolBinding, ...(name === 'generate_image' ? { toolCallId: typeof params.callId === 'string' ? params.callId : crypto.randomUUID() } : {}), conversationId: run.request.conversationId, browserTabId: run.request.browserTabId, knowledgeCapabilityToken: run.request.knowledgeCapabilityToken } const tools = this.proxyToolCatalogs.get(sessionId as string) ?? [] const tool = tools.find( (candidate) => candidate.name === name && (candidate.source === 'mcp' || isMainImageTool(candidate.name) || isStoryGraphTool(candidate.name) || isMainWebTool(candidate)) ) if (!tool) { throw new Error( 'DeepSeek Harness 请求了未知 Main 代理工具' ) } const inputSchema = proxyToolInputSchema(tool) try { assertObjectJsonSchema(inputSchema) } catch (error) { throw new Error( 'DeepSeek Harness MCP 工具参数结构不受支持', { cause: error } ) } const violations = validateJsonSchemaValue( inputSchema, argumentsValue ) if (violations.length > 0) { throw new Error( `DeepSeek Harness MCP 工具参数无效:${violations .slice(0, 5) .join('; ') .slice(0, 1_000)}` ) } const result = await run.toolProvider.callTool( name, argumentsValue as Record, run.toolController.signal, context ) return { content: result.parts.map((part) => part.type === 'text' ? { type: 'text', text: part.text } : { type: 'text', text: `[${part.mimeType} image result omitted]` } ) } } throw new Error( `不支持的 DeepSeek Harness 扩展请求:${method}` ) }, extNotification: async (method, params) => { if (method === GOODBUDDY_EVENT) { this.onBridgeEvent(params) } } } }, sdk.ndJsonStream(child.stdin, child.stdout) ) if (!agent) { throw new Error('DeepSeek Harness ACP 客户端初始化失败') } const stateWithoutCapabilities = { workspace, child, connection, agent } void this.consumeStderr(child) void child.exited.then( (result) => this.handleExit(result), () => this.fail( new Error('DeepSeek Harness 无法获取进程退出状态') ) ) void connection.closed.then( () => { if (!this.disposed) { this.fail(new Error('DeepSeek Harness ACP 连接已关闭')) } }, () => this.fail(new Error('DeepSeek Harness ACP 连接异常关闭')) ) const initialization = await withTimeout( stateWithoutCapabilities.agent.initialize({ protocolVersion: sdk.PROTOCOL_VERSION, clientCapabilities: {}, clientInfo: { name: 'GoodBuddy', version: '1' } }), this.initializationTimeoutMs, 'ACP 初始化' ) const advertisedImageInput = Boolean( initialization && typeof initialization === 'object' && ( initialization as { agentCapabilities?: { promptCapabilities?: { image?: unknown } } } ).agentCapabilities?.promptCapabilities?.image === true ) if ( advertisedImageInput !== (this.options.supportsImageInput === true) ) { throw new Error( 'DeepSeek Harness Host 图片能力与所选模型连接不一致' ) } const capabilities = this.parseCapabilities( await withTimeout( stateWithoutCapabilities.agent.extMethod( GOODBUDDY_HANDSHAKE, { controlProtocolVersion: GOODBUDDY_CONTROL_PROTOCOL_VERSION } ), this.initializationTimeoutMs, '内部控制面握手' ), sdk.PROTOCOL_VERSION ) const state = { ...stateWithoutCapabilities, capabilities } if (this.disposed) { throw new Error('DeepSeek Harness Runtime 已关闭') } this.state = state return state } catch (error) { launchController.abort(error) if (child) { await this.terminate(child) } throw error } finally { if (this.launchController === launchController) { this.launchController = undefined } } } private async getState(): Promise { if (this.fatalError) { throw this.fatalError } if (this.state) { return this.state } this.initialization ??= this.initialize() try { return await this.initialization } catch (error) { this.initialization = undefined throw error } } async getStatus(): Promise { return { id: 'deepseek-harness', label: 'DeepSeek Harness', available: !this.disposed && !this.fatalError, supportsToolExecution: true, detail: this.disposed ? 'DeepSeek Harness Runtime 已关闭' : this.fatalError?.message ?? (this.state ? `DeepSeek Harness ${this.state.capabilities.harnessVersion} · 当前用户权限` : 'DeepSeek Harness 已配置,将在首次使用时启动') } } get hasRetainedSessions(): boolean { return this.sessions.size > 0 || this.sessionInitializations.size > 0 } private toolsFor(workspace: string): ModelToolProviderLike | undefined { if (!this.options.createToolProvider) return this.options.toolProvider let provider = this.workspaceTools.get(workspace) if (!provider) { provider = this.options.createToolProvider(workspace) this.workspaceTools.set(workspace, provider) } return provider } async testConnection(): Promise { try { await this.getState() return { id: 'deepseek-harness', label: 'DeepSeek Harness', available: true, supportsToolExecution: true, detail: `DeepSeek Harness ${this.state?.capabilities.harnessVersion ?? ''} · 当前用户权限` } } catch (error) { return { id: 'deepseek-harness', label: 'DeepSeek Harness', available: false, supportsToolExecution: true, detail: error instanceof Error ? error.message : 'DeepSeek Harness 不可用' } } } async getNativeSnapshot(workspace?: string): Promise { const state = await this.getState() const response = await withTimeout( state.agent.extMethod( GOODBUDDY_NATIVE_SNAPSHOT, workspace ? { workspace } : {} ), this.initializationTimeoutMs, '原生能力清单' ) const assignedSkillIds = new Set( (this.options.skillPackages ?? []).map((skill) => skill.id) ) const rawSkills = Array.isArray(response.skills) ? response.skills : [] const skills = rawSkills .filter( ( candidate ): candidate is Record => { if ( !candidate || typeof candidate !== 'object' || Array.isArray(candidate) ) { return false } const skill = candidate as Record return ( typeof skill.id === 'string' && typeof skill.name === 'string' && !assignedSkillIds.has(skill.id.trim()) ) } ) .flatMap((skill) => { const source = typeof skill.source === 'string' ? skill.source : '' const mappedSource = source === 'project-dsh' || source === 'project-agents' ? ('workspace' as const) : source === 'user-dsh' || source === 'user-agents' ? ('global' as const) : source === 'runtime' ? ('runtime' as const) : source === 'custom' ? ('plugin' as const) : ('unknown' as const) const description = typeof skill.description === 'string' ? skill.description.trim() : '' const parsed = runtimeNativeSkillSchema.safeParse({ id: skill.id, name: skill.name, ...(description ? { description } : {}), source: mappedSource }) return parsed.success ? [parsed.data] : [] }) .slice(0, runtimeNativeInventoryLimits.skills) const rawTools = Array.isArray(response.tools) ? response.tools : [] const toolsSupported = response.toolsSupported === true && Array.isArray(response.tools) const tools = rawTools .flatMap((candidate) => { if ( !candidate || typeof candidate !== 'object' || Array.isArray(candidate) ) { return [] } const tool = candidate as Record if ( typeof tool.id !== 'string' || typeof tool.name !== 'string' ) { return [] } const id = tool.id.trim() const builtinKind = DSH_BUILTIN_TOOL_KINDS[id] const description = typeof tool.description === 'string' ? tool.description.trim() : '' const parsed = runtimeNativeToolSchema.safeParse({ id, name: tool.name, ...(description ? { description } : {}), kind: builtinKind ?? 'other', source: id === 'skill' ? 'skill' : builtinKind ? 'runtime' : 'plugin' }) return parsed.success ? [parsed.data] : [] }) .slice(0, MAX_NATIVE_TOOLS) return { provider: 'deepseek-harness', available: true, inventoryStatus: toolsSupported ? 'available' : 'partial', detail: toolsSupported ? '显示 DeepSeek Harness Host 与插件原生能力;GoodBuddy 分配的 Skill 和 MCP 不在此清单中。' : 'DeepSeek Harness 已连接,但工具清单暂不可用;GoodBuddy 分配的 Skill 和 MCP 不在原生清单中。', agents: [], tools, toolsSupported, commands: [], lsp: [], formatters: [], mcpServers: [], skills, rules: [], prompts: [], resources: [], resourcesSupported: false, context: { strategy: 'unsupported', manualCompact: false, detail: 'DeepSeek Harness 暂不支持原生上下文压缩。' } } } private async acquireConversation( conversationId: string, signal: AbortSignal ): Promise<() => void> { signal.throwIfAborted() const previous = this.conversationTails.get(conversationId) ?? Promise.resolve() let release!: () => void const gate = new Promise((resolve) => { release = resolve }) const tail = previous.then( () => gate, () => gate ) this.conversationTails.set(conversationId, tail) const aborted = new Promise((_resolve, reject) => { const onAbort = (): void => reject(signal.reason) signal.addEventListener('abort', onAbort, { once: true }) previous.finally(() => signal.removeEventListener('abort', onAbort) ) }) try { await Promise.race([previous, aborted]) signal.throwIfAborted() return () => { release() if (this.conversationTails.get(conversationId) === tail) { this.conversationTails.delete(conversationId) } } } catch (error) { release() if (this.conversationTails.get(conversationId) === tail) { this.conversationTails.delete(conversationId) } throw error } } private async getSession( state: HarnessState, conversationId: string, workspace: string ): Promise<{ id: string; created: boolean }> { const current = this.sessions.get(conversationId) if (current?.workspace === workspace) { return { id: current.id, created: false } } if (current) { await this.releaseConversation(conversationId) } const pending = this.sessionInitializations.get(conversationId) if (pending) { return { id: await pending, created: false } } const creation = state.agent .newSession({ cwd: workspace, mcpServers: [] }) .then((response) => { if (!response.sessionId) { throw new Error('DeepSeek Harness 未返回 ACP 会话 ID') } this.sessions.set(conversationId, { id: response.sessionId, workspace }) return response.sessionId }) this.sessionInitializations.set(conversationId, creation) try { return { id: await creation, created: true } } finally { this.sessionInitializations.delete(conversationId) } } private async waitForUpdate(run: ActiveRun): Promise { if (run.updates.length > 0 || run.closed || this.fatalError) { return } await new Promise((resolve) => { run.wake = resolve }) } private toRuntimeEvent( requestId: string, update: AcpSessionNotification['update'], toolNames: ActiveRun['toolNames'] ): RuntimeEvent | undefined { if (update.goodBuddyEvent) { return this.toUsageEvent( requestId, update.goodBuddyEvent ) } if ( (update.sessionUpdate === 'agent_message_chunk' || update.sessionUpdate === 'agent_thought_chunk') && update.content?.type === 'text' && update.content.text ) { return { requestId, type: update.sessionUpdate === 'agent_thought_chunk' ? 'reasoning' : 'text', delta: update.content.text } } if ( (update.sessionUpdate === 'tool_call' || update.sessionUpdate === 'tool_call_update') && update.toolCallId ) { const state = update.status === 'in_progress' ? 'running' : update.status === 'completed' ? 'completed' : update.status === 'failed' ? 'failed' : 'pending' const reportedName = ( update.name ?? update.title ?? 'DeepSeek Harness 工具' ).slice(0, 200) const callId = update.toolCallId.slice(0, 256) const previous = toolNames.get(callId) const name = reportedName === 'tool' || (!update.name && !update.title) ? previous?.name ?? reportedName : reportedName const title = toolOperationSummary( undefined, update.title === 'tool' || update.title === update.name || update.title === previous?.name ? undefined : update.title ) ?? previous?.title const input = safeStringify(update.rawInput) ?? previous?.input const summary = toolOperationSummary(update.rawInput, title) ?? previous?.summary if (state === 'pending' || state === 'running') { toolNames.set(callId, { name, input, summary, title }) } else { toolNames.delete(callId) } const output = safeStringify(update.rawOutput) return { requestId, type: 'tool', callId, name, state, summary: summary ?? `DeepSeek Harness 工具:${name}`, ...(input ? { input } : {}), ...(output ? { output } : {}) } } return undefined } async *run( request: AgentExecutionRequest, signal: AbortSignal, authorize?: RuntimeAuthorizer ): AsyncGenerator { signal.throwIfAborted() if ( request.images?.length && this.options.supportsImageInput !== true ) { throw new Error('当前 DeepSeek Harness 模型连接未启用图像输入') } const release = await this.acquireConversation( request.conversationId, signal ) let state: HarnessState | undefined let sessionId: string | undefined let run: ActiveRun | undefined const toolController = new AbortController() const abortTools = (): void => { toolController.abort(signal.reason) } signal.addEventListener('abort', abortTools, { once: true }) try { state = await this.getState() signal.throwIfAborted() const workspace = request.executionWorkspace ? await realpath(request.executionWorkspace) : state.workspace const session = await this.getSession( state, request.conversationId, workspace ) sessionId = session.id // A reused Harness session already retains earlier turns natively. const includeHistory = session.created run = { request, toolProvider: this.toolsFor(workspace), toolController, authorize, updates: [], toolNames: new Map(), closed: false } this.activeRuns.set(sessionId, run) yield { requestId: request.requestId, type: 'status', message: 'DeepSeek Harness 正在处理请求' } let completed = false let response: | { stopReason?: string } | undefined let promptError: unknown const cancel = (): void => { run!.closed = true run!.wake?.() run!.wake = undefined void state!.agent .cancel({ sessionId: sessionId! }) .catch(() => undefined) } signal.addEventListener('abort', cancel, { once: true }) if (signal.aborted) { cancel() } try { await withTimeout( state.agent.extMethod(GOODBUDDY_PREPARE, { sessionId, requestId: request.requestId }), this.initializationTimeoutMs, '请求准备' ) signal.throwIfAborted() } catch (error) { signal.removeEventListener('abort', cancel) throw error } const promptOperation = state.agent.prompt({ sessionId, prompt: [ { type: 'text', text: promptWithUntrustedConversationHistory( request, includeHistory ) }, ...(request.images ?? []).map((image) => ({ type: 'image' as const, data: image.data, mimeType: image.mediaType })) ] }) const prompt = ( this.promptTimeoutMs === undefined ? promptOperation : withTimeout( promptOperation, this.promptTimeoutMs, '请求' ) ) .then((value) => { response = value }) .catch((error: unknown) => { promptError = harnessPromptError(error) }) .finally(() => { completed = true run!.wake?.() run!.wake = undefined }) try { while (!completed || run.updates.length > 0) { signal.throwIfAborted() if (this.fatalError) { throw this.fatalError } if (run.closed && !completed) { throw new Error( 'DeepSeek Harness 请求在完成前已关闭' ) } if (run.updates.length === 0) { await this.waitForUpdate(run) continue } const update = run.updates.shift()! const event = this.toRuntimeEvent( request.requestId, update, run.toolNames ) if (event) { yield event } } await prompt if (promptError) { cancel() throw promptError } if (response?.stopReason === 'cancelled') { throw signal.aborted ? signal.reason : new Error('DeepSeek Harness 请求已取消') } yield { requestId: request.requestId, type: 'done', sessionId } } finally { if (!completed) { cancel() } signal.removeEventListener('abort', cancel) if (completed) { await prompt } else { await withTimeout( prompt, this.shutdownTimeoutMs, '取消请求' ).catch(async () => { await withTimeout( state!.agent.extMethod(GOODBUDDY_RELEASE, { sessionId }), this.shutdownTimeoutMs, '释放已取消会话' ).catch(async () => { await this.terminate(state!.child) }) await this.forgetSession(request.conversationId) }) } } } finally { signal.removeEventListener('abort', abortTools) toolController.abort( new Error('DeepSeek Harness 请求工具上下文已关闭') ) if (run) { run.closed = true run.toolController.abort( new Error('DeepSeek Harness 请求工具上下文已关闭') ) } if (sessionId && this.activeRuns.get(sessionId) === run) { this.activeRuns.delete(sessionId) } release() } } async releaseConversation(conversationId: string): Promise { await this.sessionInitializations.get(conversationId)?.catch(() => undefined) const session = this.sessions.get(conversationId) const sessionId = session?.id if (sessionId && this.state) { await this.state.agent .extMethod(GOODBUDDY_RELEASE, { sessionId }) .catch(async () => { await this.state?.agent .cancel({ sessionId }) .catch(() => undefined) }) } await this.forgetSession(conversationId) } private async forgetSession(conversationId: string): Promise { const session = this.sessions.get(conversationId) const provider = session ? this.workspaceTools.get(session.workspace) ?? this.options.toolProvider : this.options.toolProvider this.sessions.delete(conversationId) if (session) this.proxyToolCatalogs.delete(session.id) await provider ?.releaseConversation(conversationId) .catch(() => undefined) if ( session && provider && this.workspaceTools.get(session.workspace) === provider && ![...this.sessions.values()].some((other) => other.workspace === session.workspace) ) { this.workspaceTools.delete(session.workspace) await provider.dispose() } } async dispose(): Promise { if (this.disposed) { return } this.disposed = true this.launchController?.abort( new Error('DeepSeek Harness Runtime 已关闭') ) this.launchController = undefined const state = this.state this.state = undefined this.initialization = undefined for (const [sessionId, run] of this.activeRuns) { run.closed = true run.toolController.abort( new Error('DeepSeek Harness Runtime 已关闭') ) run.wake?.() run.wake = undefined if (state) { void state.agent .cancel({ sessionId }) .catch(() => undefined) } } this.activeRuns.clear() this.sessions.clear() this.proxyToolCatalogs.clear() this.sessionInitializations.clear() this.conversationTails.clear() const providers = new Set([ ...this.workspaceTools.values(), ...(this.options.toolProvider ? [this.options.toolProvider] : []) ]) this.workspaceTools.clear() const disposeTools = () => Promise.allSettled( [...providers].map((provider) => provider.dispose()) ) if (!state) { await disposeTools() return } await withTimeout( state.agent.extMethod(GOODBUDDY_SHUTDOWN, {}), this.shutdownTimeoutMs, '内部控制面关闭' ).catch(() => undefined) await disposeTools() await this.terminate(state.child) await withTimeout( Promise.allSettled([ state.child.exited, state.connection.closed ]).then(() => undefined), this.shutdownTimeoutMs, '关闭' ).catch(() => undefined) } } import { imageToolDescriptionLimit } from '../../shared/image-generation-contracts' import { isStoryGraphTool } from '../../shared/story-graph-tools'