Files
goodbuddy/src/main/agent/goodbuddy-harness-control-plane.ts
T
mesalogo 66f77b83c2 feat: let agents read supervisor story graphs
Expose saved decisions, timelines and source evidence through read-only Story Graph tools across Model, Harness and local/remote OpenCode and Continue. Supervisor enablement and runtime assignments govern discovery and calls; reads do not invoke models or modify stored evidence. Remote use requires matching Desktop and Agent changes.

Make Continue discussion show the complete editable message with a separated destination and shared action layout. Sending uses the visible draft without reattaching removed context.

Validation: existing focused MCP, runtime, controlled Linux Host, discussion component and Electron layout checks plus typechecks; staged diff check passed. Cross-scope fact reuse, historical reconstruction and full package acceptance remain deferred.

Release note: Agents can consult saved supervisor decisions and their sources when continuing work. Continue discussion now lets you review and edit the complete message before sending.
2026-09-28 12:38:00 +08:00

1458 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) || isStoryGraphTool(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'
import { isStoryGraphTool } from '../../shared/story-graph-tools'