Files
goodbuddy/src/main/agent/goodbuddy-harness-control-plane.ts
T
mesalogo f81f342cde feat: open local runtime clients from the composer
Add composer shortcuts for local Continue and OpenCode terminals and the official DeepSeek Harness Web client, using the current project and model bridge. Manage browser service reuse, stop actions and terminal cleanup. Upgrade DeepSeek Harness to 0.1.7-rc.2 and Cordis to 4.0.4.

Include the coordinated IPC handler extraction and related tests and documentation. Remote native-client coordinator wiring, managed Node consolidation, Ask MCP mappings and full desktop/package acceptance remain unfinished; this commit records the implemented local workflow and supporting SSH groundwork.

Validation: typecheck and lint passed; 267 focused tests passed and 5 skipped. No new real-provider calls were made for this commit. Prior real-model evidence and remaining acceptance work are documented in the feature progress files.

Release note: 本地项目选择 Continue 或 OpenCode 后,可从输入框下方打开原生终端;选择 DS 后,可打开使用当前项目和模型的官方 Web 客户端。客户端使用独立会话,DS 网页关闭后服务继续运行,可在 GoodBuddy 中停止;远程入口尚未完成。
2026-09-27 08:38:00 +08:00

1457 lines
40 KiB
TypeScript

import { randomUUID } from 'node:crypto'
import { isAbsolute } from 'node:path'
import {
AgentSideConnection,
PROTOCOL_VERSION,
RequestError,
type Agent,
type AgentSideConnection as AcpAgentConnection,
type ContentBlock as AcpContentBlock,
type Stream
} from '@agentclientprotocol/sdk'
import type { Context } from '@deepseek-ai/cordis'
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
import {
AttachmentError,
isImageAdmissionError,
type ImageAttachmentRef,
type SaveImageAttachment
} from '@deepseek-ai/dsh-attachment'
import {
CredentialProvider,
type CredentialInfo,
type CredentialKey,
type CredentialRecord,
type CredentialRecordEntry,
type CredentialRecordInfo,
type CredentialRef,
type ResolvedCredential
} from '@deepseek-ai/dsh-credentials'
import {
createUserMessage,
errorChain,
type ContentBlock as HarnessContentBlock,
type TokenUsage
} from '@deepseek-ai/dsh-llm'
import {
SessionId,
type SessionEvent
} from '@deepseek-ai/dsh-session'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
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 { GoodBuddyHarnessAttachmentStore } from './goodbuddy-harness-attachment-store'
export {
GOODBUDDY_CONTROL_PROTOCOL_VERSION,
GOODBUDDY_CREDENTIAL,
GOODBUDDY_EVENT,
GOODBUDDY_HANDSHAKE,
GOODBUDDY_NATIVE_SNAPSHOT,
GOODBUDDY_RELEASE,
GOODBUDDY_SHUTDOWN,
GOODBUDDY_TOOLS_CALL,
GOODBUDDY_TOOLS_LIST,
GOODBUDDY_PREPARE
} from './deepseek-harness-protocol'
const DEFAULT_MAX_EVENT_CHARACTERS = 64 * 1024
const DELTA_BATCH_CHARACTERS = 4 * 1024
const DELTA_BATCH_INTERVAL_MS = 100
const MAX_SUMMARY_CHARACTERS = 4_000
const MAX_MCP_PROXY_RESULT_BYTES = 256 * 1024
const MAX_NATIVE_SKILLS = 200
const MAX_NATIVE_TOOLS = 200
const NATIVE_SNAPSHOT_TIMEOUT_MS = 2_000
const MAIN_WEB_TOOL_NAMES = new Set(['web_search', 'web_fetch'])
const GOODBUDDY_EXECUTION_GUIDANCE = [
'GoodBuddy work mode rules:',
'- In Execute mode, act through the available tools instead of writing a long implementation plan.',
'- Inspect only what is needed, then create or update the requested workspace files promptly.',
'- Work in small verifiable steps and use tool results as the source of truth.',
'- Keep reasoning concise. Do not narrate code that can be written and checked with tools.',
'- In Ask mode, remain read-only and do not attempt mutations.'
].join('\n')
export type GoodBuddyWorkMode = 'ask' | 'execute'
export type GoodBuddyHarnessCapabilities = {
controlProtocolVersion: 1
harnessVersion: string
acpProtocolVersion: number
supports: {
cancellation: true
sessionRelease: true
reasoningEvents: boolean
toolEvents: boolean
usageEvents: boolean
credentialResolution: true
}
execution: {
mode: 'host'
}
}
export type GoodBuddyHarnessControlConfig = {
provider: string
model: string
supportsImageInput?: boolean
workspace: string
harnessVersion: string
execution: GoodBuddyHarnessCapabilities['execution']
credentialRefs: readonly string[]
skills: readonly {
name: string
description: string
content: string
directory: string
}[]
trustedAskToolDefinitions?: ReadonlyMap<string, ToolDefinition>
stream?: Stream
maxEventCharacters?: number
}
type Preparation = {
requestId: string
mode: GoodBuddyWorkMode
}
type OwnedSession = {
handle: AgentHandle
attachmentRefs: ImageAttachmentRef[]
preparation?: Preparation
proxyTools: Map<
string,
{
definition: ToolDefinition
dispose: () => void
}
>
askToolDefinitions: Map<string, ToolDefinition>
inflight?: {
requestId: string
messageId: string
mode: GoodBuddyWorkMode
turn?: number
endReason?: string
turnError?: unknown
resolve: (reason: string) => void
reject: (error: unknown) => void
eventTail: Promise<void>
eventError?: unknown
pendingDelta?: {
type: 'text' | 'reasoning'
delta: string
}
pendingDeltaTimer?: ReturnType<typeof setTimeout>
}
}
type ProxyToolDefinition = {
name: string
description: string
inputSchema: Record<string, unknown>
}
function validateProxyToolResult(
value: unknown
): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value)
) {
throw new Error('GoodBuddy MCP tool result is invalid')
}
const serialized = JSON.stringify(value)
if (
Buffer.byteLength(serialized, 'utf8') >
MAX_MCP_PROXY_RESULT_BYTES
) {
throw new Error(
'GoodBuddy MCP tool result exceeds safety limit'
)
}
const content = (value as Record<string, unknown>).content
if (
!Array.isArray(content) ||
content.length > 100 ||
content.some(
(part) =>
!part ||
typeof part !== 'object' ||
Array.isArray(part) ||
(part as Record<string, unknown>).type !== 'text' ||
typeof (part as Record<string, unknown>).text !== 'string'
)
) {
throw new Error('GoodBuddy MCP tool result is invalid')
}
return value as Record<string, unknown>
}
function parseProxyToolCatalog(
value: unknown
): ProxyToolDefinition[] {
if (!Array.isArray(value) || value.length > 100) {
throw new Error('GoodBuddy MCP tool catalog is invalid')
}
const names = new Set<string>()
return value.map((candidate) => {
if (
!candidate ||
typeof candidate !== 'object' ||
Array.isArray(candidate)
) {
throw new Error('GoodBuddy MCP tool definition is invalid')
}
const tool = candidate as Record<string, unknown>
if (
typeof tool.name !== 'string' ||
!/^[a-zA-Z0-9_-]{1,64}$/u.test(tool.name) ||
names.has(tool.name) ||
typeof tool.description !== 'string' ||
tool.description.length > (tool.name === 'generate_image' ? imageToolDescriptionLimit : 1_000) ||
!tool.inputSchema ||
typeof tool.inputSchema !== 'object' ||
Array.isArray(tool.inputSchema)
) {
throw new Error('GoodBuddy MCP tool definition is invalid')
}
names.add(tool.name)
return {
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema as Record<string, unknown>
}
})
}
type CredentialResolver = (
ref: string
) => Promise<string | undefined>
/**
* Memory-only credential provider. It deliberately has no writable operation
* and can resolve only references registered by the trusted host.
*/
export class GoodBuddyCredentialProvider extends CredentialProvider {
private resolver?: CredentialResolver
constructor(
ctx: Context,
private readonly allowedRefs: ReadonlySet<string>
) {
super(ctx)
}
bind(resolver: CredentialResolver): void {
if (this.resolver) {
throw new Error('GoodBuddy credential resolver is already bound')
}
this.resolver = resolver
}
async resolve(
ref: CredentialRef
): Promise<ResolvedCredential | undefined> {
if (!this.allowedRefs.has(ref) || !this.resolver) {
return undefined
}
const value = await this.resolver(ref)
return value ? { value, source: 'goodbuddy-main' } : undefined
}
async describe(ref: CredentialRef): Promise<CredentialInfo> {
const available =
this.allowedRefs.has(ref) && this.resolver !== undefined
return {
configured: available,
source: available ? 'goodbuddy-main' : undefined,
writable: false
}
}
async set(ref: CredentialRef, value: string): Promise<void> {
void ref
void value
throw new Error('GoodBuddy Harness credentials are read-only')
}
async unset(ref: CredentialRef): Promise<void> {
void ref
throw new Error('GoodBuddy Harness credentials are read-only')
}
async readRecord(
key: CredentialKey
): Promise<CredentialRecord | undefined> {
void key
return undefined
}
async describeRecord(
key: CredentialKey
): Promise<CredentialRecordInfo> {
void key
return { configured: false, writable: false }
}
async listRecords(): Promise<readonly CredentialRecordEntry[]> {
return []
}
async modifyRecord(
key: CredentialKey,
mutate: (
current: CredentialRecord | undefined
) => Promise<CredentialRecord | undefined>
): Promise<CredentialRecord | undefined> {
void key
void mutate
throw new Error('GoodBuddy Harness credentials are read-only')
}
async deleteRecord(key: CredentialKey): Promise<void> {
void key
throw new Error('GoodBuddy Harness credentials are read-only')
}
}
export function createBoundedAcpStream(
stream: Stream,
maxFrameBytes: number
): Stream {
if (
!Number.isSafeInteger(maxFrameBytes) ||
maxFrameBytes < 1
) {
throw new TypeError('maxFrameBytes must be a positive integer')
}
return {
readable: stream.readable.pipeThrough(
new TransformStream({
transform(message, controller) {
if (
Buffer.byteLength(JSON.stringify(message), 'utf8') >
maxFrameBytes
) {
throw new Error(
`ACP input frame exceeds ${maxFrameBytes} bytes`
)
}
controller.enqueue(message)
}
})
),
writable: new WritableStream({
async write(message) {
if (
Buffer.byteLength(JSON.stringify(message), 'utf8') >
maxFrameBytes
) {
throw new Error(
`ACP output frame exceeds ${maxFrameBytes} bytes`
)
}
const writer = stream.writable.getWriter()
try {
await writer.write(message)
} finally {
writer.releaseLock()
}
},
async close() {
const writer = stream.writable.getWriter()
try {
await writer.close()
} finally {
writer.releaseLock()
}
},
async abort(reason) {
await stream.writable.abort(reason)
}
})
}
}
function requiredString(
params: Record<string, unknown>,
key: string,
maxLength = 512
): string {
const value = params[key]
if (
typeof value !== 'string' ||
value.length === 0 ||
value.length > maxLength
) {
throw RequestError.invalidParams(
undefined,
`${key} must be a non-empty bounded string`
)
}
return value
}
function boundedJson(value: unknown): string | undefined {
if (value === undefined) {
return undefined
}
try {
return (
typeof value === 'string'
? value
: JSON.stringify(value)
).slice(0, MAX_SUMMARY_CHARACTERS)
} catch {
return '[unserializable]'
}
}
function promptText(prompt: readonly AcpContentBlock[]): string {
const text: string[] = []
for (const block of prompt) {
switch (block.type) {
case 'text':
text.push(block.text)
break
case 'image':
case 'resource_link':
break
case 'audio':
case 'resource':
throw RequestError.invalidParams(
undefined,
`unsupported ACP prompt content: ${block.type}`
)
default:
block satisfies never
}
}
return text.join('')
}
function turnReason(event: SessionEvent): string | undefined {
if (event.type !== 'turn/end') {
return undefined
}
switch (event.data.reason.kind) {
case 'aborted':
case 'interrupted':
return 'cancelled'
case 'max-tokens':
return 'max_tokens'
default:
return 'end_turn'
}
}
export class GoodBuddyHarnessControlPlane {
private readonly sessions = new Map<string, OwnedSession>()
private readonly allowedCredentialRefs: ReadonlySet<string>
private readonly maxEventCharacters: number
private connection?: AcpAgentConnection
private credentialProvider?: GoodBuddyCredentialProvider
private handshaken = false
private observing = false
private closed = false
private disposing?: Promise<void>
constructor(
private readonly ctx: Context,
private readonly config: GoodBuddyHarnessControlConfig
) {
this.allowedCredentialRefs = new Set(config.credentialRefs)
this.maxEventCharacters =
config.maxEventCharacters ?? DEFAULT_MAX_EVENT_CHARACTERS
}
bindCredentialProvider(
provider: GoodBuddyCredentialProvider
): void {
if (this.connection) {
throw new Error(
'Credential provider must be bound before bridge start'
)
}
this.credentialProvider = provider
}
start(): AcpAgentConnection {
if (this.connection) {
throw new Error(
'GoodBuddy Harness control plane is already started'
)
}
if (!this.config.stream) {
throw new Error(
'GoodBuddy Harness control plane requires its internal Host transport'
)
}
const connection = new AgentSideConnection(
() => this.createAgentApi(),
this.config.stream
)
this.connection = connection
this.credentialProvider?.bind(async (ref) => {
if (!this.handshaken || !this.allowedCredentialRefs.has(ref)) {
return undefined
}
const response = await connection.extMethod(
GOODBUDDY_CREDENTIAL,
{ ref }
)
const value = response.value
return typeof value === 'string' && value.length > 0
? value
: undefined
})
void connection.closed.finally(() => this.dispose())
return connection
}
private capabilities(): GoodBuddyHarnessCapabilities {
return {
controlProtocolVersion: GOODBUDDY_CONTROL_PROTOCOL_VERSION,
harnessVersion: this.config.harnessVersion,
acpProtocolVersion: PROTOCOL_VERSION,
supports: {
cancellation: true,
sessionRelease: true,
reasoningEvents: true,
toolEvents: true,
usageEvents: true,
credentialResolution: true
},
execution: this.config.execution
}
}
private requireSession(sessionId: string): OwnedSession {
const session = this.sessions.get(sessionId)
if (!session) {
throw RequestError.invalidParams(
undefined,
`unknown session: ${sessionId}`
)
}
return session
}
private async sendEvent(
sessionId: string,
event: Record<string, unknown>
): Promise<void> {
const record = this.sessions.get(sessionId)
const inflight = record?.inflight
if (!inflight || !this.connection) {
return
}
const params = {
sessionId,
requestId: inflight.requestId,
...event
}
const text = JSON.stringify(params)
if (text.length > this.maxEventCharacters) {
throw new Error(
'GoodBuddy Harness control event exceeds safety limit'
)
}
await this.connection.extNotification(GOODBUDDY_EVENT, params)
}
private enqueueEvent(
sessionId: string,
event: Record<string, unknown>
): void {
const record = this.sessions.get(sessionId)
const inflight = record?.inflight
if (!record || !inflight) {
return
}
const previous = inflight.eventTail
const queued = previous.then(() =>
this.sendEvent(sessionId, event)
)
inflight.eventTail = queued.catch((error: unknown) => {
inflight.eventError ??= error
record.handle.agent.cancel({ kind: 'user' })
})
}
private flushPendingDelta(sessionId: string): void {
const inflight = this.sessions.get(sessionId)?.inflight
const pending = inflight?.pendingDelta
if (!inflight || !pending) {
return
}
if (inflight.pendingDeltaTimer) {
clearTimeout(inflight.pendingDeltaTimer)
inflight.pendingDeltaTimer = undefined
}
inflight.pendingDelta = undefined
this.enqueueEvent(sessionId, pending)
}
private maxDeltaBatchCharacters(
sessionId: string,
type: 'text' | 'reasoning'
): number {
const inflight = this.sessions.get(sessionId)?.inflight
if (!inflight) {
return 0
}
const envelopeLength = JSON.stringify({
sessionId,
requestId: inflight.requestId,
type,
delta: ''
}).length
return Math.min(
DELTA_BATCH_CHARACTERS,
this.maxEventCharacters - envelopeLength
)
}
private queueDelta(
sessionId: string,
type: 'text' | 'reasoning',
delta: string
): void {
const inflight = this.sessions.get(sessionId)?.inflight
if (!inflight || !delta) {
return
}
const batchLimit = this.maxDeltaBatchCharacters(
sessionId,
type
)
if (batchLimit < 1) {
this.failEventStream(
sessionId,
new Error(
'GoodBuddy Harness control event exceeds safety limit'
)
)
return
}
let remaining = delta
while (remaining) {
if (
inflight.pendingDelta &&
inflight.pendingDelta.type !== type
) {
this.flushPendingDelta(sessionId)
}
inflight.pendingDelta ??= { type, delta: '' }
const available =
batchLimit - inflight.pendingDelta.delta.length
if (available <= 0) {
this.flushPendingDelta(sessionId)
continue
}
inflight.pendingDelta.delta += remaining.slice(0, available)
remaining = remaining.slice(available)
if (inflight.pendingDelta.delta.length >= batchLimit) {
this.flushPendingDelta(sessionId)
}
}
if (
inflight.pendingDelta &&
!inflight.pendingDeltaTimer
) {
inflight.pendingDeltaTimer = setTimeout(() => {
this.flushPendingDelta(sessionId)
}, DELTA_BATCH_INTERVAL_MS)
}
}
private queueEvent(
sessionId: string,
event: Record<string, unknown>
): void {
this.flushPendingDelta(sessionId)
this.enqueueEvent(sessionId, event)
}
private failEventStream(sessionId: string, error: unknown): void {
const record = this.sessions.get(sessionId)
const inflight = record?.inflight
if (!record || !inflight) {
return
}
inflight.eventError ??= error
record.handle.agent.cancel({ kind: 'user' })
}
private observeSessions(): void {
if (this.observing) {
return
}
this.observing = true
this.ctx.on('tools/execute', async (exec, next) => {
const sessionId = exec.agent?.session.id
const record = sessionId
? this.sessions.get(sessionId)
: undefined
if (
record &&
record.handle.agent === exec.agent &&
record.inflight?.mode === 'ask'
) {
const registeredDefinition =
record.askToolDefinitions.get(exec.name)
const executingDefinition =
record.handle.agent.ctx.tools.get(
exec.name,
exec.agent
)
if (
!registeredDefinition ||
executingDefinition !== registeredDefinition
) {
throw new Error(
`Ask 模式不允许执行非只读工具:${exec.name}`
)
}
}
return next()
})
this.ctx.on('agent/assistant-stream', ({ agent, frame }) => {
const record = this.sessions.get(agent.session.id)
if (
record?.handle.agent !== agent ||
!record.inflight ||
frame.type !== 'chunk'
) {
return
}
const chunk = frame.chunk
if (chunk.type === 'text-delta' || chunk.type === 'reasoning-delta') {
this.queueDelta(
agent.session.id,
chunk.type === 'text-delta' ? 'text' : 'reasoning',
chunk.text
)
} else if (chunk.type === 'usage') {
this.queueUsage(agent.session.id, chunk.usage)
}
})
this.ctx.on(
'session/event',
(session, event: SessionEvent) => {
const record = this.sessions.get(session.header.id)
if (!record || record.handle.agent.session !== session) {
return
}
const inflight = record.inflight
if (!inflight) {
return
}
if (event.type === 'tool/call') {
this.queueEvent(session.header.id, {
type: 'tool',
callId: event.data.callId,
name: event.data.name,
state: 'pending',
input: event.data.arguments.slice(
0,
MAX_SUMMARY_CHARACTERS
)
})
} else if (event.type === 'tool/result') {
const toolResult = event.data.message
this.queueEvent(session.header.id, {
type: 'tool',
callId: toolResult.toolCallId,
name: 'tool',
state:
event.data.error || toolResult.isError === true
? 'failed'
: 'completed',
output: boundedJson(event.data.message.content)
})
}
if (
event.type === 'turn/end' &&
inflight.turn === event.data.turn
) {
inflight.endReason = turnReason(event)
}
}
)
this.ctx.on(
'agent/inbox/claimed',
({ agent, message, turn }) => {
const record = this.sessions.get(agent.session.id)
if (
record?.handle.agent === agent &&
record.inflight?.messageId === message.id
) {
record.inflight.turn = turn
}
}
)
this.ctx.on(
'agent/error',
({ agent, error }) => {
const record = this.sessions.get(agent.session.id)
if (record?.handle.agent === agent) {
if (record.inflight) {
record.inflight.turnError = error
}
}
}
)
}
private queueUsage(sessionId: string, usage: TokenUsage): void {
this.flushPendingDelta(sessionId)
this.enqueueEvent(sessionId, {
type: 'model-usage',
callId: randomUUID(),
provider: this.config.provider,
model: this.config.model,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
cacheReadTokens: usage.cacheReadTokens ?? 0,
cacheWriteTokens: usage.cacheWriteTokens ?? 0
})
}
private proxyToolDefinition(
sessionId: string,
tool: ProxyToolDefinition
): ToolDefinition {
return {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
output: {
schema: {
type: 'object',
properties: {
content: {
type: 'array',
items: {
type: 'object',
additionalProperties: true
}
}
},
required: ['content'],
additionalProperties: false
},
render(_args, result) {
const content = (
result as {
content: Array<{
type?: unknown
text?: unknown
}>
}
).content
return content
.filter(
(
part
): part is {
type: 'text'
text: string
} =>
part.type === 'text' &&
typeof part.text === 'string'
)
.map((part) => ({
type: 'text' as const,
text: part.text
}))
}
},
execute: async (argumentsValue) => {
const result = await this.connection!.extMethod(
GOODBUDDY_TOOLS_CALL,
{
sessionId,
name: tool.name,
callId: randomUUID(),
arguments: argumentsValue
}
)
return validateProxyToolResult(result)
}
}
}
private async refreshProxyTools(
sessionId: string,
record: OwnedSession
): Promise<void> {
const response = await this.connection!.extMethod(
GOODBUDDY_TOOLS_LIST,
{ sessionId }
)
const tools = parseProxyToolCatalog(response.tools)
const nextNames = new Set(tools.map((tool) => tool.name))
for (const [name, registration] of record.proxyTools) {
if (!nextNames.has(name)) {
registration.dispose()
record.proxyTools.delete(name)
record.askToolDefinitions.delete(name)
}
}
for (const tool of tools) {
if (!record.proxyTools.has(tool.name)) {
const definition = this.proxyToolDefinition(sessionId, tool)
const dispose =
record.handle.agent.ctx.tools.register(definition)
record.proxyTools.set(tool.name, {
definition,
dispose
})
if (MAIN_WEB_TOOL_NAMES.has(tool.name)) {
record.askToolDefinitions.set(tool.name, definition)
}
}
}
}
private async nativeSnapshot(
workspace = this.config.workspace
): Promise<Record<string, unknown>> {
const controller = new AbortController()
const timer = setTimeout(
() =>
controller.abort(
new Error('DeepSeek Harness native inventory timed out')
),
NATIVE_SNAPSHOT_TIMEOUT_MS
)
try {
const skills = await Promise.race([
this.ctx.skills.list({
cwd: workspace,
signal: controller.signal
}),
new Promise<never>((_resolve, reject) => {
controller.signal.addEventListener(
'abort',
() => reject(controller.signal.reason),
{ once: true }
)
})
])
let toolsSupported = true
let tools: Array<{
id: string
name: string
description?: string
}> = []
try {
tools = this.ctx.tools
.schemas()
.slice(0, MAX_NATIVE_TOOLS)
.flatMap((tool) => {
const name = tool.name.trim().slice(0, 128)
if (!name) {
return []
}
const description = tool.description.trim().slice(0, 2_000)
return [
{
id: name,
name: name.slice(0, 200),
...(description ? { description } : {})
}
]
})
} catch {
toolsSupported = false
}
return {
tools,
toolsSupported,
skills: skills.slice(0, MAX_NATIVE_SKILLS).map((skill) => ({
id: skill.name.slice(0, 128),
name: skill.name.slice(0, 200),
description: skill.description.slice(0, 2_000),
source: skill.source.slice(0, 128),
provider: skill.provider.slice(0, 128)
}))
}
} finally {
clearTimeout(timer)
}
}
private attachmentStore(): GoodBuddyHarnessAttachmentStore {
const store = this.ctx.get('attachments')
if (!(store instanceof GoodBuddyHarnessAttachmentStore)) {
throw RequestError.internalError(
undefined,
'Harness image attachment service is unavailable'
)
}
return store
}
private releaseAttachments(refs: readonly ImageAttachmentRef[]): void {
if (refs.length === 0) {
return
}
const store = this.attachmentStore()
for (const ref of refs) {
store.releaseImage(ref)
}
}
private async storePromptImages(
prompt: readonly AcpContentBlock[]
): Promise<ImageAttachmentRef[]> {
const imageBlocks = prompt.filter(
(
block
): block is Extract<AcpContentBlock, { type: 'image' }> =>
block.type === 'image'
)
if (imageBlocks.length === 0) {
return []
}
if (!this.config.supportsImageInput) {
throw RequestError.invalidParams(
undefined,
'the selected model does not accept image input'
)
}
const store = this.attachmentStore()
if (imageBlocks.length > store.imageLimits.maxImagesPerMessage) {
throw RequestError.invalidParams(
undefined,
'too many images in one prompt'
)
}
const inputs: SaveImageAttachment[] = []
let totalBytes = 0
for (const block of imageBlocks) {
if (
block.uri != null ||
!store.imageLimits.mediaTypes.includes(
block.mimeType as SaveImageAttachment['mediaType']
) ||
block.data.length === 0 ||
block.data.length % 4 !== 0 ||
block.data.length >
Math.ceil(store.imageLimits.maxImageBytes / 3) * 4 ||
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
block.data
)
) {
throw RequestError.invalidParams(
undefined,
'invalid inline image prompt content'
)
}
const data = Buffer.from(block.data, 'base64')
totalBytes += data.byteLength
if (
data.byteLength === 0 ||
data.byteLength > store.imageLimits.maxImageBytes ||
totalBytes > store.imageLimits.maxMessageImageBytes ||
data.toString('base64') !== block.data
) {
throw RequestError.invalidParams(
undefined,
'invalid inline image prompt content'
)
}
inputs.push({
data,
mediaType: block.mimeType as SaveImageAttachment['mediaType']
})
}
try {
return await store.saveImages(inputs)
} catch (error) {
if (error instanceof AttachmentError) {
if (isImageAdmissionError(error)) {
throw RequestError.invalidParams(
undefined,
'invalid inline image prompt content'
)
}
if (error.code === 'ATTACHMENT_WRITE_FAILED') {
throw RequestError.internalError(
undefined,
'Harness image attachment service is unavailable'
)
}
}
throw RequestError.internalError(
undefined,
'Harness image attachment storage failed'
)
}
}
private createAgentApi(): Agent {
this.observeSessions()
return {
initialize: async () => ({
protocolVersion: PROTOCOL_VERSION,
agentInfo: {
name: 'goodbuddy-deepseek-harness',
version: this.config.harnessVersion
},
agentCapabilities: {
promptCapabilities: {
image: this.config.supportsImageInput === true,
audio: false,
embeddedContext: false
},
sessionCapabilities: {
close: {}
}
},
authMethods: []
}),
authenticate: async () => undefined,
newSession: async (params) => {
if (this.closed) {
throw RequestError.internalError(
undefined,
'bridge is shutting down'
)
}
if (
!isAbsolute(params.cwd) ||
params.mcpServers.length > 0
) {
throw RequestError.invalidParams(
undefined,
'an absolute workspace and no MCP servers are required'
)
}
const sessionId = SessionId(randomUUID())
let genuineSkillDefinition: ToolDefinition | undefined
const handle = await this.ctx.agents.create({
sessionId,
meta: { cwd: params.cwd },
agentOptions: {
provider: this.config.provider,
model: this.config.model
},
setup: async (agentCtx) => {
agentCtx.systemPrompt.section({
name: 'goodbuddy:controlled-execution',
order: 50,
text: GOODBUDDY_EXECUTION_GUIDANCE
})
const skillRegistrations = agentCtx.inject(
['skills'],
(skillCtx) => {
for (const skill of this.config.skills) {
skillCtx.skills.register({
name: skill.name,
description: skill.description,
content: skill.content,
source: 'bundled',
resourceBase: {
kind: 'directory',
path: skill.directory
},
invocation: {
modelInvocable: true,
userInvocable: true
}
})
}
}
)
await agentCtx.plugin(ToolSkill)
genuineSkillDefinition =
agentCtx.tools.get('skill')
await skillRegistrations
}
})
const askToolDefinitions = new Map(
this.config.trustedAskToolDefinitions ?? []
)
if (genuineSkillDefinition) {
askToolDefinitions.set(
'skill',
genuineSkillDefinition
)
}
this.sessions.set(sessionId, {
handle,
attachmentRefs: [],
proxyTools: new Map(),
askToolDefinitions
})
return {
sessionId,
modes: {
currentModeId: 'ask',
availableModes: [
{
id: 'ask',
name: 'Ask',
description: 'Read-only'
}
]
}
}
},
prompt: async (params) => {
const record = this.requireSession(params.sessionId)
if (record.inflight) {
throw RequestError.invalidParams(
undefined,
'a prompt is already in flight'
)
}
const preparation = record.preparation
record.preparation = undefined
if (!preparation) {
throw RequestError.invalidParams(
undefined,
'a single-use goodbuddy/session/prepare is required'
)
}
await this.refreshProxyTools(params.sessionId, record)
const text = promptText(params.prompt)
if (!text.trim()) {
throw RequestError.invalidParams(
undefined,
'empty prompt'
)
}
const attachmentRefs = await this.storePromptImages(
params.prompt
)
let message: ReturnType<typeof createUserMessage>
try {
const content: HarnessContentBlock[] = [
{ type: 'text', text },
...attachmentRefs.map((attachment) => ({
type: 'image' as const,
attachment
}))
]
message = createUserMessage({
content,
source: { kind: 'user' }
})
} catch (error) {
this.releaseAttachments(attachmentRefs)
throw error
}
const stopReason = await new Promise<string>(
(resolve, reject) => {
record.inflight = {
requestId: preparation.requestId,
messageId: message.id,
mode: preparation.mode,
resolve,
reject,
eventTail: Promise.resolve()
}
try {
record.handle.agent.followup(message)
} catch (error) {
record.inflight = undefined
this.releaseAttachments(attachmentRefs)
reject(error)
return
}
record.attachmentRefs.push(...attachmentRefs)
void record.handle.agent.whenIdle().then(() => {
const current = record.inflight
if (current?.messageId !== message.id) {
return
}
this.flushPendingDelta(params.sessionId)
void current.eventTail.then(() => {
if (record.inflight === current) {
record.inflight = undefined
}
if (current.eventError) {
current.reject(current.eventError)
return
}
if (current.turnError) {
const details = errorChain(
current.turnError
).slice(0, MAX_SUMMARY_CHARACTERS)
current.reject(
RequestError.internalError(
{ details },
`DeepSeek Harness turn failed: ${details}`
)
)
return
}
current.resolve(current.endReason ?? 'cancelled')
})
}, (error) => {
if (record.inflight?.messageId === message.id) {
record.inflight = undefined
}
reject(error)
})
}
)
return {
stopReason:
stopReason === 'cancelled'
? 'cancelled'
: stopReason === 'max_tokens'
? 'max_tokens'
: 'end_turn'
}
},
cancel: async ({ sessionId }) => {
const record = this.sessions.get(sessionId)
record?.handle.agent.cancel({ kind: 'user' })
if (record?.inflight) {
const inflight = record.inflight
if (inflight.pendingDeltaTimer) {
clearTimeout(inflight.pendingDeltaTimer)
}
record.inflight = undefined
inflight.resolve('cancelled')
}
},
closeSession: async ({ sessionId }) => {
await this.releaseSession(sessionId)
},
extMethod: (method, params) =>
this.extensionMethod(method, params),
extNotification: async () => {
throw RequestError.methodNotFound('extension notification')
}
}
}
async extensionMethod(
method: string,
params: Record<string, unknown>
): Promise<Record<string, unknown>> {
if (method === GOODBUDDY_HANDSHAKE) {
const version = params.controlProtocolVersion
if (version !== GOODBUDDY_CONTROL_PROTOCOL_VERSION) {
throw RequestError.invalidParams(
undefined,
'incompatible GoodBuddy Harness control protocol'
)
}
this.handshaken = true
return this.capabilities() as unknown as Record<
string,
unknown
>
}
if (!this.handshaken) {
throw RequestError.invalidParams(
undefined,
'GoodBuddy handshake is required'
)
}
if (method === GOODBUDDY_PREPARE) {
const sessionId = requiredString(params, 'sessionId')
const requestId = requiredString(params, 'requestId')
const mode = params.mode
if (mode !== 'ask' && mode !== 'execute') {
throw RequestError.invalidParams(
undefined,
'mode must be ask or execute'
)
}
const record = this.requireSession(sessionId)
if (record.inflight || record.preparation) {
throw RequestError.invalidParams(
undefined,
'session is already prepared or running'
)
}
record.preparation = { requestId, mode }
return { prepared: true }
}
if (method === GOODBUDDY_RELEASE) {
await this.releaseSession(
requiredString(params, 'sessionId')
)
return { released: true }
}
if (method === GOODBUDDY_NATIVE_SNAPSHOT) {
const workspace = params.workspace === undefined
? this.config.workspace
: requiredString(params, 'workspace')
if (!isAbsolute(workspace)) throw RequestError.invalidParams(undefined, 'workspace must be absolute')
return this.nativeSnapshot(workspace)
}
if (method === GOODBUDDY_SHUTDOWN) {
await this.dispose()
return { shutdown: true }
}
throw RequestError.methodNotFound(method)
}
async releaseSession(sessionId: string): Promise<void> {
const record = this.sessions.get(sessionId)
if (!record) {
return
}
this.sessions.delete(sessionId)
record.handle.agent.cancel({ kind: 'user' })
if (record.inflight) {
if (record.inflight.pendingDeltaTimer) {
clearTimeout(record.inflight.pendingDeltaTimer)
}
record.inflight.resolve('cancelled')
record.inflight = undefined
}
try {
await record.handle.dispose()
} finally {
this.releaseAttachments(record.attachmentRefs)
}
}
async dispose(): Promise<void> {
this.disposing ??= (async () => {
this.closed = true
const sessions = [...this.sessions.entries()]
this.sessions.clear()
for (const [, record] of sessions) {
record.handle.agent.cancel({ kind: 'disposed' })
if (record.inflight?.pendingDeltaTimer) {
clearTimeout(record.inflight.pendingDeltaTimer)
}
record.inflight?.resolve('cancelled')
}
await Promise.allSettled(
sessions.map(([, record]) => record.handle.dispose())
)
for (const [, record] of sessions) {
this.releaseAttachments(record.attachmentRefs)
}
})()
await this.disposing
}
}
import { imageToolDescriptionLimit } from '../../shared/image-generation-contracts'