1398 lines
38 KiB
TypeScript
1398 lines
38 KiB
TypeScript
import { createHash, randomUUID } from 'node:crypto'
|
|
import { isAbsolute } from 'node:path'
|
|
import {
|
|
AgentSideConnection,
|
|
PROTOCOL_VERSION,
|
|
RequestError,
|
|
type Agent,
|
|
type AgentSideConnection as AcpAgentConnection,
|
|
type Stream
|
|
} from '@agentclientprotocol/sdk'
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type { AgentHandle } from '@deepseek-ai/dsh-agent'
|
|
import {
|
|
CredentialProvider,
|
|
type CredentialInfo,
|
|
type CredentialRef,
|
|
type ResolvedCredential
|
|
} from '@deepseek-ai/dsh-credentials'
|
|
import {
|
|
createUserMessage,
|
|
errorChain,
|
|
type TokenUsage
|
|
} from '@deepseek-ai/dsh-llm'
|
|
import {
|
|
SessionId,
|
|
type SessionEvent
|
|
} from '@deepseek-ai/dsh-session'
|
|
import { setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
|
|
import { setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
|
|
import type {
|
|
ToolDefinition,
|
|
ToolExecution,
|
|
ToolExecutionResult
|
|
} from '@deepseek-ai/dsh-tools'
|
|
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
|
|
|
|
export const GOODBUDDY_CONTROL_PROTOCOL_VERSION = 1
|
|
export const GOODBUDDY_HANDSHAKE = 'goodbuddy/handshake'
|
|
export const GOODBUDDY_PREPARE = 'goodbuddy/session/prepare'
|
|
export const GOODBUDDY_RELEASE = 'goodbuddy/session/release'
|
|
export const GOODBUDDY_EVENT = 'goodbuddy/session/event'
|
|
export const GOODBUDDY_CREDENTIAL = 'goodbuddy/credential/resolve'
|
|
export const GOODBUDDY_TOOLS_LIST = 'goodbuddy/tools/list'
|
|
export const GOODBUDDY_TOOLS_CALL = 'goodbuddy/tools/call'
|
|
export const GOODBUDDY_SHUTDOWN = 'goodbuddy/shutdown'
|
|
|
|
const DEFAULT_MAX_EVENT_CHARACTERS = 64 * 1024
|
|
const DEFAULT_MAX_REQUEST_CHARACTERS = 4 * 1024 * 1024
|
|
export const GOODBUDDY_HARNESS_MAX_STEP_TOKENS = 16 * 1024
|
|
const DELTA_BATCH_CHARACTERS = 4 * 1024
|
|
const DELTA_BATCH_INTERVAL_MS = 100
|
|
const MAX_SUMMARY_CHARACTERS = 4_000
|
|
const MAX_FINGERPRINT_BYTES = 4 * 1024 * 1024
|
|
const MAX_MCP_PROXY_RESULT_BYTES = 256 * 1024
|
|
const GOODBUDDY_EXECUTION_GUIDANCE = [
|
|
'GoodBuddy controlled execution 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
|
|
oneShotApproval: true
|
|
reasoningEvents: boolean
|
|
toolEvents: boolean
|
|
usageEvents: boolean
|
|
credentialResolution: true
|
|
}
|
|
sandbox: {
|
|
provider: string
|
|
enforcement: 'full' | 'partial'
|
|
}
|
|
}
|
|
|
|
export type GoodBuddyHarnessControlConfig = {
|
|
provider: string
|
|
model: string
|
|
workspace: string
|
|
harnessVersion: string
|
|
sandbox: GoodBuddyHarnessCapabilities['sandbox']
|
|
credentialRefs: readonly string[]
|
|
skills: readonly {
|
|
name: string
|
|
description: string
|
|
content: string
|
|
directory: string
|
|
}[]
|
|
stream?: Stream
|
|
maxEventCharacters?: number
|
|
maxRequestCharacters?: number
|
|
}
|
|
|
|
type Preparation = {
|
|
requestId: string
|
|
mode: GoodBuddyWorkMode
|
|
}
|
|
|
|
type OwnedSession = {
|
|
handle: AgentHandle
|
|
preparation?: Preparation
|
|
proxyToolDisposers: Map<string, () => void>
|
|
sandboxRetries: GoodBuddySandboxRetryLedger
|
|
inflight?: {
|
|
requestId: string
|
|
messageId: string
|
|
turn?: number
|
|
endReason?: string
|
|
turnError?: unknown
|
|
resolve: (reason: string) => void
|
|
reject: (error: unknown) => void
|
|
emittedCharacters: number
|
|
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 > 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 DeniedToolCall = {
|
|
toolName: string
|
|
operationFingerprint: string
|
|
}
|
|
|
|
type CredentialResolver = (
|
|
ref: string
|
|
) => Promise<string | undefined>
|
|
|
|
function argumentsFingerprint(value: unknown): string | undefined {
|
|
try {
|
|
const serialized = JSON.stringify(value, (_key, nested) => {
|
|
if (
|
|
nested &&
|
|
typeof nested === 'object' &&
|
|
!Array.isArray(nested)
|
|
) {
|
|
return Object.fromEntries(
|
|
Object.entries(nested as Record<string, unknown>).sort(
|
|
([left], [right]) => left.localeCompare(right)
|
|
)
|
|
)
|
|
}
|
|
return nested
|
|
})
|
|
if (
|
|
serialized === undefined ||
|
|
Buffer.byteLength(serialized, 'utf8') >
|
|
MAX_FINGERPRINT_BYTES
|
|
) {
|
|
return undefined
|
|
}
|
|
return createHash('sha256').update(serialized).digest('hex')
|
|
} catch {
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
function isSandboxDenial(
|
|
result: Readonly<ToolExecutionResult>
|
|
): boolean {
|
|
const sandboxValue =
|
|
!result.isError &&
|
|
result.value &&
|
|
typeof result.value === 'object' &&
|
|
!Array.isArray(result.value)
|
|
? (result.value as Record<string, unknown>).sandbox
|
|
: undefined
|
|
return (
|
|
(result.isError &&
|
|
result.error.info?.code === 'FS_SANDBOX_DENIED') ||
|
|
result.content.some(
|
|
(content) =>
|
|
content.type === 'text' &&
|
|
content.text.includes('[sandbox: file access denied under ')
|
|
) ||
|
|
(!!sandboxValue &&
|
|
typeof sandboxValue === 'object' &&
|
|
!Array.isArray(sandboxValue) &&
|
|
(sandboxValue as Record<string, unknown>).denied === true)
|
|
)
|
|
}
|
|
|
|
function requestedEscalation(
|
|
value: unknown
|
|
): {
|
|
mode: 'workspace-write' | 'danger-full-access'
|
|
operationFingerprint: string
|
|
} | undefined {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value)
|
|
) {
|
|
return undefined
|
|
}
|
|
const argumentsRecord = value as Record<string, unknown>
|
|
const mode = argumentsRecord.sandbox_permissions
|
|
if (
|
|
(mode !== 'workspace-write' &&
|
|
mode !== 'danger-full-access') ||
|
|
typeof argumentsRecord.justification !== 'string' ||
|
|
!argumentsRecord.justification.trim()
|
|
) {
|
|
return undefined
|
|
}
|
|
const operationArguments = { ...argumentsRecord }
|
|
delete operationArguments.sandbox_permissions
|
|
delete operationArguments.justification
|
|
const operationFingerprint = argumentsFingerprint(
|
|
operationArguments
|
|
)
|
|
return operationFingerprint
|
|
? {
|
|
mode,
|
|
operationFingerprint
|
|
}
|
|
: undefined
|
|
}
|
|
|
|
export class GoodBuddySandboxRetryLedger {
|
|
private readonly deniedToolCalls = new Map<
|
|
string,
|
|
DeniedToolCall
|
|
>()
|
|
|
|
clear(): void {
|
|
this.deniedToolCalls.clear()
|
|
}
|
|
|
|
record(
|
|
execution: Readonly<ToolExecution>,
|
|
result: Readonly<ToolExecutionResult>
|
|
): void {
|
|
if (!isSandboxDenial(result)) {
|
|
return
|
|
}
|
|
const operationFingerprint = argumentsFingerprint(
|
|
execution.arguments
|
|
)
|
|
if (!operationFingerprint) {
|
|
return
|
|
}
|
|
this.deniedToolCalls.set(execution.callId, {
|
|
toolName: execution.name,
|
|
operationFingerprint
|
|
})
|
|
}
|
|
|
|
consumeRetry(toolName: string, argumentsValue: unknown): boolean {
|
|
const escalation = requestedEscalation(argumentsValue)
|
|
if (escalation?.mode !== 'danger-full-access') {
|
|
return false
|
|
}
|
|
const denied = [...this.deniedToolCalls.entries()]
|
|
.reverse()
|
|
.find(
|
|
([, candidate]) =>
|
|
candidate.toolName === toolName &&
|
|
candidate.operationFingerprint ===
|
|
escalation.operationFingerprint
|
|
)
|
|
if (!denied) {
|
|
return false
|
|
}
|
|
this.deniedToolCalls.delete(denied[0])
|
|
return true
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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')
|
|
}
|
|
}
|
|
|
|
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 { type: string; text?: string }[]
|
|
): string {
|
|
if (
|
|
prompt.some(
|
|
(block) =>
|
|
block.type !== 'text' &&
|
|
block.type !== 'resource_link'
|
|
)
|
|
) {
|
|
throw RequestError.invalidParams(
|
|
undefined,
|
|
'only text and resource_link prompt content is supported'
|
|
)
|
|
}
|
|
return prompt
|
|
.map((block) => (block.type === 'text' ? block.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 readonly maxRequestCharacters: 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
|
|
this.maxRequestCharacters =
|
|
config.maxRequestCharacters ??
|
|
DEFAULT_MAX_REQUEST_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,
|
|
oneShotApproval: true,
|
|
reasoningEvents: true,
|
|
toolEvents: true,
|
|
usageEvents: true,
|
|
credentialResolution: true
|
|
},
|
|
sandbox: this.config.sandbox
|
|
}
|
|
}
|
|
|
|
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'
|
|
)
|
|
}
|
|
inflight.emittedCharacters += text.length
|
|
if (
|
|
inflight.emittedCharacters > this.maxRequestCharacters
|
|
) {
|
|
record.handle.agent.cancel({ kind: 'user' })
|
|
throw new Error(
|
|
'GoodBuddy Harness control request output 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(
|
|
'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 === 'assistant/chunk') {
|
|
const chunk = event.data.chunk
|
|
if (
|
|
chunk.type === 'text-delta' ||
|
|
chunk.type === 'reasoning-delta'
|
|
) {
|
|
this.queueDelta(
|
|
session.header.id,
|
|
chunk.type === 'text-delta'
|
|
? 'text'
|
|
: 'reasoning',
|
|
chunk.text
|
|
)
|
|
} else if (chunk.type === 'usage') {
|
|
this.queueUsage(session.header.id, chunk.usage)
|
|
}
|
|
} else 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.content.find(
|
|
(content) => content.type === 'tool-result'
|
|
)
|
|
this.queueEvent(session.header.id, {
|
|
type: 'tool',
|
|
callId: toolResult?.toolCallId ?? 'unknown-tool-call',
|
|
name: 'tool',
|
|
state: event.data.error ? '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
|
|
}
|
|
}
|
|
}
|
|
)
|
|
this.ctx.on(
|
|
'tools/result',
|
|
(
|
|
exec: Readonly<ToolExecution>,
|
|
result: Readonly<ToolExecutionResult>
|
|
) => {
|
|
const sessionId = exec.agent?.session.id
|
|
if (!sessionId) {
|
|
return
|
|
}
|
|
const record = this.sessions.get(sessionId)
|
|
if (
|
|
record?.handle.agent !== exec.agent ||
|
|
!record.inflight
|
|
) {
|
|
return
|
|
}
|
|
record.sandboxRetries.record(exec, result)
|
|
}
|
|
)
|
|
this.ctx.on('approval/request', async (request, next) => {
|
|
const record = this.sessions.get(request.agent.session.id)
|
|
if (
|
|
!record ||
|
|
record.handle.agent !== request.agent ||
|
|
!record.inflight ||
|
|
!this.connection
|
|
) {
|
|
return next()
|
|
}
|
|
const matchingRetry = request.callId
|
|
? record.handle.agent.session.events
|
|
.filter(
|
|
(
|
|
event
|
|
): event is Extract<
|
|
SessionEvent,
|
|
{ type: 'tool/call' }
|
|
> =>
|
|
event.type === 'tool/call' &&
|
|
event.data.callId === request.callId
|
|
)
|
|
.at(-1)
|
|
: undefined
|
|
let retryArguments: unknown
|
|
if (matchingRetry) {
|
|
try {
|
|
retryArguments = JSON.parse(matchingRetry.data.arguments)
|
|
} catch {
|
|
return 'rejected'
|
|
}
|
|
}
|
|
if (
|
|
!matchingRetry ||
|
|
!record.sandboxRetries.consumeRetry(
|
|
request.toolName,
|
|
retryArguments
|
|
)
|
|
) {
|
|
return 'rejected'
|
|
}
|
|
const response = await this.connection.requestPermission({
|
|
sessionId: request.agent.session.id,
|
|
toolCall: {
|
|
toolCallId:
|
|
request.callId ?? `approval-${randomUUID()}`,
|
|
title: request.reason ?? request.toolName
|
|
},
|
|
options: [
|
|
{
|
|
optionId: 'allow-once',
|
|
name: 'Allow once',
|
|
kind: 'allow_once'
|
|
},
|
|
{
|
|
optionId: 'reject-once',
|
|
name: 'Reject',
|
|
kind: 'reject_once'
|
|
}
|
|
]
|
|
})
|
|
return response.outcome.outcome === 'selected' &&
|
|
response.outcome.optionId === 'allow-once'
|
|
? 'allowed-once'
|
|
: 'rejected'
|
|
})
|
|
}
|
|
|
|
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,
|
|
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, dispose] of record.proxyToolDisposers) {
|
|
if (!nextNames.has(name)) {
|
|
dispose()
|
|
record.proxyToolDisposers.delete(name)
|
|
}
|
|
}
|
|
for (const tool of tools) {
|
|
if (!record.proxyToolDisposers.has(tool.name)) {
|
|
record.proxyToolDisposers.set(
|
|
tool.name,
|
|
record.handle.agent.ctx.tools.register(
|
|
this.proxyToolDefinition(sessionId, tool)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private createAgentApi(): Agent {
|
|
this.observeSessions()
|
|
return {
|
|
initialize: async () => ({
|
|
protocolVersion: PROTOCOL_VERSION,
|
|
agentInfo: {
|
|
name: 'goodbuddy-deepseek-harness',
|
|
version: this.config.harnessVersion
|
|
},
|
|
agentCapabilities: {
|
|
promptCapabilities: {
|
|
image: false,
|
|
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.cwd !== this.config.workspace ||
|
|
params.mcpServers.length > 0
|
|
) {
|
|
throw RequestError.invalidParams(
|
|
undefined,
|
|
'the controlled workspace and no MCP servers are required'
|
|
)
|
|
}
|
|
const sessionId = SessionId(randomUUID())
|
|
const handle = await this.ctx.agents.create({
|
|
sessionId,
|
|
meta: { cwd: params.cwd },
|
|
agentOptions: {
|
|
provider: this.config.provider,
|
|
model: this.config.model,
|
|
maxTokens: GOODBUDDY_HARNESS_MAX_STEP_TOKENS
|
|
},
|
|
setup: async (agentCtx) => {
|
|
agentCtx.systemPrompt.section({
|
|
name: 'goodbuddy:controlled-execution',
|
|
order: 50,
|
|
text: GOODBUDDY_EXECUTION_GUIDANCE
|
|
})
|
|
const skillTool = agentCtx.plugin(ToolSkill)
|
|
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 Promise.all([skillTool, skillRegistrations])
|
|
}
|
|
})
|
|
setSandboxMode(handle.agent.session, 'read-only')
|
|
setApprovalPolicy(handle.agent.session, 'never')
|
|
this.sessions.set(sessionId, {
|
|
handle,
|
|
proxyToolDisposers: new Map(),
|
|
sandboxRetries: new GoodBuddySandboxRetryLedger()
|
|
})
|
|
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'
|
|
)
|
|
}
|
|
setSandboxMode(
|
|
record.handle.agent.session,
|
|
preparation.mode === 'ask'
|
|
? 'read-only'
|
|
: 'workspace-write'
|
|
)
|
|
setApprovalPolicy(
|
|
record.handle.agent.session,
|
|
preparation.mode === 'ask' ? 'never' : 'ask'
|
|
)
|
|
if (preparation.mode === 'execute') {
|
|
await this.refreshProxyTools(params.sessionId, record)
|
|
} else {
|
|
for (const dispose of record.proxyToolDisposers.values()) {
|
|
dispose()
|
|
}
|
|
record.proxyToolDisposers.clear()
|
|
}
|
|
record.sandboxRetries.clear()
|
|
const text = promptText(params.prompt)
|
|
if (!text.trim()) {
|
|
throw RequestError.invalidParams(
|
|
undefined,
|
|
'empty prompt'
|
|
)
|
|
}
|
|
const message = createUserMessage({
|
|
content: [{ type: 'text', text }],
|
|
source: { kind: 'user' }
|
|
})
|
|
const stopReason = await new Promise<string>(
|
|
(resolve, reject) => {
|
|
record.inflight = {
|
|
requestId: preparation.requestId,
|
|
messageId: message.id,
|
|
resolve,
|
|
reject,
|
|
emittedCharacters: 0,
|
|
eventTail: Promise.resolve()
|
|
}
|
|
try {
|
|
record.handle.agent.followup(message)
|
|
} catch (error) {
|
|
record.inflight = undefined
|
|
reject(error)
|
|
return
|
|
}
|
|
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_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
|
|
}
|
|
await record.handle.dispose()
|
|
}
|
|
|
|
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())
|
|
)
|
|
})()
|
|
await this.disposing
|
|
}
|
|
}
|