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
+148 -1
View File
@@ -504,6 +504,75 @@ describe('ModelAgentRuntime', () => {
)
})
it('manually compacts Continue history even when automatic compression is disabled', async () => {
const fetcher = vi.fn<typeof fetch>().mockResolvedValueOnce(
new Response(createEventStream('手动压缩摘要'), {
status: 200,
headers: { 'content-type': 'text/event-stream' }
})
)
const runtime = new ModelAgentRuntime({
apiKey: 'test-key',
baseUrl: 'https://bigtoken.ai',
model: 'sonnet-5',
protocol: 'anthropic-messages',
authentication: 'api-key',
fetcher,
contextCompression: {
settings: {
enabled: false,
triggerTokens: 20_000,
recentRawTokens: 5_000,
modelSource: { kind: 'current' },
summaryPrompt: 'Summarize earlier history.'
},
contextWindowTokens: 32_000
}
})
const history = [
{ role: 'user' as const, content: 'earlier question' },
{ role: 'assistant' as const, content: 'earlier answer' },
{ role: 'user' as const, content: 'recent question' },
{ role: 'assistant' as const, content: 'recent answer' }
]
const outcome = await runtime.compactConversation(
{
requestId: '00000000-0000-4000-8000-000000000081',
conversationId: '00000000-0000-4000-8000-000000000082',
runtimeSelection: { provider: 'continue' },
history,
historyMessageIds: [
'00000000-0000-4000-8000-000000000083',
'00000000-0000-4000-8000-000000000084',
'00000000-0000-4000-8000-000000000085',
'00000000-0000-4000-8000-000000000086'
]
},
new AbortController().signal
)
expect(fetcher).toHaveBeenCalledOnce()
const body = JSON.parse(
fetcher.mock.calls[0]![1]!.body as string
) as { messages: unknown[] }
expect(JSON.stringify(body.messages)).toContain('earlier question')
expect(JSON.stringify(body.messages)).not.toContain('recent question')
expect(outcome.result).toMatchObject({
provider: 'continue',
strategy: 'goodbuddy-summary',
compacted: true,
contextCompressionState: {
coveredMessageCount: 2,
coveredFromMessageId:
'00000000-0000-4000-8000-000000000083',
coveredThroughMessageId:
'00000000-0000-4000-8000-000000000084',
summary: '手动压缩摘要'
}
})
await runtime.dispose()
})
it('waits for completed provider usage before normal threshold compression', async () => {
const fetcher = vi
.fn<typeof fetch>()
@@ -551,7 +620,7 @@ describe('ModelAgentRuntime', () => {
contextWindowTokens: 32_000
}
})
const events = []
const events: RuntimeEvent[] = []
for await (const event of runtime.run(
{
@@ -2593,6 +2662,84 @@ describe('ModelAgentRuntime', () => {
expect(toolProvider.callTool).toHaveBeenCalledTimes(2)
})
it('continues without tools when the refreshed inventory is empty', async () => {
const progressTool: ModelToolDefinition = {
name: 'record_progress',
displayName: 'Record progress',
description: 'Record the final required step',
inputSchema: { type: 'object' },
source: 'builtin'
}
const listTools = vi
.fn<ModelToolProviderLike['listTools']>()
.mockResolvedValueOnce([progressTool])
.mockResolvedValueOnce([])
const toolProvider = createToolProvider({ listTools })
const responses = [
{
choices: [{
message: {
role: 'assistant',
content: null,
tool_calls: [{
id: 'call-progress',
type: 'function',
function: {
name: progressTool.name,
arguments: '{}'
}
}]
}
}]
},
{
choices: [{
message: {
role: 'assistant',
content: '进度记录完成。'
}
}]
}
]
const fetcher = vi.fn<typeof fetch>(async () =>
Response.json(responses.shift())
)
const runtime = new ModelAgentRuntime({
baseUrl: 'http://127.0.0.1:11434/v1',
model: 'qwen3',
protocol: 'openai-chat-completions',
authentication: 'none',
fetcher,
toolProvider
})
const events = []
for await (const event of runtime.run(
{
requestId: 'a431666e-5ec8-45e6-beb4-654132eed151',
conversationId: 'conversation-empty-refreshed-tools',
prompt: '记录进度后完成',
workMode: 'execute'
},
new AbortController().signal,
vi.fn(async () => 'once' as const)
)) {
events.push(event)
}
expect(listTools).toHaveBeenCalledTimes(2)
expect(toolProvider.callTool).toHaveBeenCalledOnce()
expect(
JSON.parse(fetcher.mock.calls[1]?.[1]?.body as string)
).not.toHaveProperty('tools')
expect(events).toContainEqual(
expect.objectContaining({
type: 'text',
delta: '进度记录完成。'
})
)
})
it('runs only scoped knowledge in Ask without requesting approval', async () => {
const responses = [
{