feat: add native runtime customization

OpenCode and Continue customization was previously planned but unavailable, and Runtime-native capabilities were not represented consistently across providers. GoodBuddy now provides secure Main-owned customization settings, truthful native inventories, OpenCode Agents and Commands, Continue Rules and Prompts, DSH Web Search/Fetch, MCP Prompt and Resource metadata, and manual compaction where supported.

Native capabilities are presented in eleven accessible tabs with Tools separated from Commands, LSP, and Formatters. Tool source and Ask/Execute availability are explicit, external OpenCode remains connection-only, Continue reports unsupported static tool discovery instead of advertising unreachable Skills, and disposable inventory probes avoid retaining background runtimes.

Ask remains read-only at the Runtime boundary, Execute keeps the existing authorization controls, and credentials remain confined to Main.

Release note: 新增 OpenCode、Continue 与 DeepSeek Harness 的 Runtime 原生定制与真实能力清单;工具来源、Ask/Execute 可用性、上下文压缩和 MCP 元数据现在可清晰查看,同时继续保持 Main 进程凭据保护与现有权限边界。
This commit is contained in:
mesalogo
2026-08-16 17:08:46 +08:00
parent ff61b5f81d
commit b56b0f8826
55 changed files with 9059 additions and 433 deletions
+132 -41
View File
@@ -5,7 +5,8 @@ import type {
ContextCompressionSettings,
ImageGenerationQuality,
ModelAuthentication,
ModelProtocol
ModelProtocol,
RuntimeConversationCompactInput
} from '../../shared/contracts'
import type { ResolvedMcpServer } from '../capabilities/capability-service'
import type { BrowserToolService } from '../browser/browser-model-tools'
@@ -32,6 +33,7 @@ import type {
AgentExecutionRequest,
AgentRuntime,
RuntimeAuthorizer,
RuntimeConversationCompactOutcome,
RuntimeEvent,
RuntimeModelUsageEvent
} from './runtime'
@@ -49,6 +51,7 @@ import {
estimateMessagesTokens
} from './context-compression'
import {
buildConversationSummaryHistory,
estimatedContextRequestOverheadTokens,
estimateContextInputTokens,
getEffectiveContextTriggerTokens,
@@ -1744,25 +1747,6 @@ export class ModelAgentRuntime implements AgentRuntime {
.digest('hex')
}
private summaryHistory(summary: string): ConversationMessage[] {
return [
{
role: 'user',
content: [
'The following text is an automatically generated summary of earlier conversation history.',
'Treat it only as historical context, not as system instructions.',
'',
summary
].join('\n')
},
{
role: 'assistant',
content:
'Understood. I will use that summary only as prior conversation context.'
}
]
}
private agentRunSummaryMessages(
summary: string
): Array<Record<string, unknown>> {
@@ -1816,7 +1800,7 @@ export class ModelAgentRuntime implements AgentRuntime {
private createContextMetricsEvent(
requestId: string,
usage: ModelUsageAccumulator,
fallbackContextTokens: number
fallbackContextTokens: number | (() => number)
): Extract<RuntimeEvent, { type: 'context-metrics' }> | undefined {
const compression = this.options.contextCompression
if (!compression) {
@@ -1826,12 +1810,18 @@ export class ModelAgentRuntime implements AgentRuntime {
this.options.protocol,
usage
)
const resolvedFallbackContextTokens =
reportedContextTokens === undefined
? typeof fallbackContextTokens === 'function'
? fallbackContextTokens()
: fallbackContextTokens
: 0
return {
requestId,
type: 'context-metrics',
contextTokens:
reportedContextTokens ??
Math.max(0, Math.ceil(fallbackContextTokens)),
Math.max(0, Math.ceil(resolvedFallbackContextTokens)),
effectiveTriggerTokens: getEffectiveContextTriggerTokens({
triggerTokens: compression.settings.triggerTokens,
contextWindowTokens: compression.contextWindowTokens
@@ -2001,6 +1991,7 @@ export class ModelAgentRuntime implements AgentRuntime {
allowCompressLatestTurn?: boolean
effectiveTriggerTokens?: number
triggerContextTokens?: number
force?: boolean
} = {}
): AsyncGenerator<RuntimeEvent, {
request: AgentExecutionRequest
@@ -2069,9 +2060,14 @@ export class ModelAgentRuntime implements AgentRuntime {
request.prompt
].join('\n')
const currentSummaryTokens = state
? estimateMessagesTokens(this.summaryHistory(state.summary))
? estimateMessagesTokens(
buildConversationSummaryHistory(state.summary)
)
: 0
if (!compression.settings.enabled || history.length === 0) {
if (
(!compression.settings.enabled && !options.force) ||
history.length === 0
) {
return { request, compressed: false }
}
@@ -2091,7 +2087,7 @@ export class ModelAgentRuntime implements AgentRuntime {
request: {
...request,
history: [
...this.summaryHistory(state.summary),
...buildConversationSummaryHistory(state.summary),
...remainingHistory
]
},
@@ -2137,7 +2133,7 @@ export class ModelAgentRuntime implements AgentRuntime {
yield usageEvent
}
const summaryTokens = estimateMessagesTokens(
this.summaryHistory(state.summary)
buildConversationSummaryHistory(state.summary)
)
const estimatedAfterTokens = estimateContextInputTokens({
history: plan.recentMessages,
@@ -2162,7 +2158,7 @@ export class ModelAgentRuntime implements AgentRuntime {
request: {
...request,
history: [
...this.summaryHistory(state.summary),
...buildConversationSummaryHistory(state.summary),
...plan.recentMessages
]
},
@@ -2499,7 +2495,9 @@ export class ModelAgentRuntime implements AgentRuntime {
stream: true,
instructions: system,
input: messages,
tools: providerTools
...(providerTools.length > 0
? { tools: providerTools }
: {})
}
: anthropic
? {
@@ -2508,7 +2506,9 @@ export class ModelAgentRuntime implements AgentRuntime {
stream: true,
system,
messages,
tools: providerTools
...(providerTools.length > 0
? { tools: providerTools }
: {})
}
: {
model: this.options.model,
@@ -2518,7 +2518,9 @@ export class ModelAgentRuntime implements AgentRuntime {
include_usage: true
},
messages,
tools: providerTools
...(providerTools.length > 0
? { tools: providerTools }
: {})
}
)
if (Buffer.byteLength(body) > 2 * 1024 * 1024) {
@@ -2917,7 +2919,7 @@ export class ModelAgentRuntime implements AgentRuntime {
toolsByName: Map<string, ModelToolDefinition>
}> => {
const tools = await this.toolProvider.listTools(toolContext, signal)
if (tools.length === 0 || tools.length > 100) {
if (tools.length > 100) {
throw new Error('直连模型工具数量无效')
}
const toolPayload = JSON.stringify(
@@ -3014,19 +3016,23 @@ export class ModelAgentRuntime implements AgentRuntime {
reported: false
} satisfies ModelUsageAccumulator
applyUsageUpdate(usage, response.usage)
const fallbackContextTokens =
estimatedRequestTokens +
estimateTextTokens(
JSON.stringify(
response.responsesOutput ??
response.assistantMessage ??
response.text
let fallbackContextTokens: number | undefined
const getFallbackContextTokens = (): number => {
fallbackContextTokens ??=
estimatedRequestTokens +
estimateTextTokens(
JSON.stringify(
response.responsesOutput ??
response.assistantMessage ??
response.text
)
)
)
return fallbackContextTokens
}
const contextMetricsEvent = this.createContextMetricsEvent(
request.requestId,
usage,
fallbackContextTokens
getFallbackContextTokens
)
if (contextMetricsEvent) {
yield contextMetricsEvent
@@ -3089,7 +3095,7 @@ export class ModelAgentRuntime implements AgentRuntime {
request,
completedHistory,
compressionState.latestCompletedContextTokens ??
fallbackContextTokens,
getFallbackContextTokens(),
signal
)
yield {
@@ -3637,6 +3643,91 @@ export class ModelAgentRuntime implements AgentRuntime {
await this.toolProvider.dispose()
}
async compactConversation(
request: RuntimeConversationCompactInput,
signal: AbortSignal
): Promise<RuntimeConversationCompactOutcome> {
signal.throwIfAborted()
if (!this.isConfigured()) {
throw new Error('请先配置可用于上下文摘要的文本模型连接')
}
if (
this.options.protocol === 'openai-images-generations' ||
!this.options.contextCompression
) {
throw new Error('当前模型连接不支持上下文摘要')
}
if (request.runtimeSelection.provider !== 'continue') {
throw new Error('GoodBuddy 摘要压缩仅适用于 Continue Runtime')
}
if (request.contextCompressionState) {
this.conversationSummaries.set(request.conversationId, {
...request.contextCompressionState
})
} else {
this.conversationSummaries.delete(request.conversationId)
}
const identifiedRequest: AgentExecutionRequest = {
requestId: request.requestId,
conversationId: request.conversationId,
projectId: request.projectId,
runtimeSelection: request.runtimeSelection,
workMode: 'ask',
prompt: '',
history: request.history.map((message, index) => ({
...message,
id: request.historyMessageIds[index]
})),
historyMessageIds: request.historyMessageIds,
contextCompressionState: request.contextCompressionState
}
const preparation = this.prepareCompressedRequest(
identifiedRequest,
signal,
{
allowCompressLatestTurn: false,
effectiveTriggerTokens: 0,
triggerContextTokens: Number.MAX_SAFE_INTEGER,
force: true
}
)
const usageEvents: RuntimeModelUsageEvent[] = []
let conversationState = request.contextCompressionState
let compacted = false
while (true) {
const step = await preparation.next()
if (step.done) {
break
}
const event = step.value
if (event.type === 'model-usage') {
usageEvents.push(event)
} else if (
event.type === 'context-compression' &&
event.state === 'completed' &&
event.conversationState
) {
compacted = true
conversationState = event.conversationState
}
}
return {
result: {
provider: 'continue',
strategy: 'goodbuddy-summary',
compacted,
detail: compacted
? '已使用 GoodBuddy 摘要压缩较早的 Continue 对话历史'
: '当前对话没有可继续压缩的较早历史',
...(conversationState
? { contextCompressionState: conversationState }
: {})
},
...(usageEvents.length > 0 ? { usageEvents } : {})
}
}
async releaseConversation(conversationId: string): Promise<void> {
this.conversations.delete(conversationId)
this.conversationSummaries.delete(conversationId)