Files
goodbuddy/src/shared/runtime-customization-contracts.ts
T
mesalogo 4baf4ec142 fix: restore manual context compaction for direct models
Local direct-model text conversations could not use manual context compaction,
and disabling automatic compression prevented saved summaries from being reused.
Restore the composer action regardless of the automatic setting or threshold,
use the selected summary connection, and persist summaries for subsequent
requests while retaining recent turns and full conversation history.

Image connections, remote channel conversations, and SSH projects remain outside
the local direct-model compaction workflow. Add focused regressions, a local
Electron end-to-end test, and an opt-in two-request live probe.

Validation: staged snapshot passed typecheck, changed-file ESLint, 13 focused
tests, and the Electron end-to-end test with two local HTTP fixture requests.
No additional real-model calls were made during commit verification.

Release note: 修复本地直连文本模型无法手动压缩上下文的问题。现在关闭自动压缩或尚未达到触发阈值时,也可点击“压缩上下文”;摘要会随会话保存并用于后续对话,同时保留最近轮次和完整历史。
2026-09-28 22:21:47 +08:00

513 lines
12 KiB
TypeScript

import { z } from 'zod'
import {
assistantIdSchema,
conversationContextCompressionStateSchema,
conversationHistoryMessageSchema
} from './assistant-contracts'
import { agentRuntimeSelectionSchema } from './runtime-selection-contracts'
export const customizableRuntimeProviderSchema = z.enum([
'opencode',
'continue',
'deepseek-harness'
])
export type CustomizableRuntimeProvider = z.infer<
typeof customizableRuntimeProviderSchema
>
export const runtimeCustomizationLimits = {
presets: 12,
rulesPerPreset: 32,
promptsPerPreset: 32,
nameCharacters: 120,
descriptionCharacters: 500,
contentCharacters: 20_000
} as const
export const runtimeNativeInventoryLimits = {
agents: 100,
tools: 200,
commands: 200,
lsp: 100,
formatters: 100,
mcpServers: 100,
skills: 200,
rules: 200,
prompts: 200,
resources: 200
} as const
export const boundedRuntimeIdentifierSchema = z
.string()
.trim()
.min(1)
.max(128)
.refine(
(value) =>
[...value].every((character) => {
const code = character.charCodeAt(0)
return code > 31 && code !== 127
}),
'Runtime 标识包含控制字符'
)
const boundedRuntimeLabelSchema = z
.string()
.trim()
.min(1)
.max(200)
const boundedRuntimeDescriptionSchema = z
.string()
.trim()
.max(2_000)
.optional()
export const continueRuleSchema = z
.object({
id: assistantIdSchema,
name: z
.string()
.trim()
.min(1)
.max(runtimeCustomizationLimits.nameCharacters),
content: z
.string()
.trim()
.min(1)
.max(runtimeCustomizationLimits.contentCharacters),
enabled: z.boolean()
})
.strict()
export type ContinueRule = z.infer<typeof continueRuleSchema>
export const runtimePromptTemplateSchema = z
.object({
id: assistantIdSchema,
name: z
.string()
.trim()
.min(1)
.max(runtimeCustomizationLimits.nameCharacters),
description: z
.string()
.trim()
.max(runtimeCustomizationLimits.descriptionCharacters)
.optional(),
prompt: z
.string()
.trim()
.min(1)
.max(runtimeCustomizationLimits.contentCharacters)
})
.strict()
export type RuntimePromptTemplate = z.infer<
typeof runtimePromptTemplateSchema
>
export const continueConfigurationPresetSchema = z
.object({
id: assistantIdSchema,
name: z
.string()
.trim()
.min(1)
.max(runtimeCustomizationLimits.nameCharacters),
description: z
.string()
.trim()
.max(runtimeCustomizationLimits.descriptionCharacters)
.optional(),
rules: z
.array(continueRuleSchema)
.max(runtimeCustomizationLimits.rulesPerPreset),
prompts: z
.array(runtimePromptTemplateSchema)
.max(runtimeCustomizationLimits.promptsPerPreset)
})
.strict()
.superRefine((preset, context) => {
const ids = [
...preset.rules.map((rule) => rule.id),
...preset.prompts.map((prompt) => prompt.id)
]
if (new Set(ids).size !== ids.length) {
context.addIssue({
code: 'custom',
message: 'Continue 预设中的规则与 Prompt ID 不得重复'
})
}
})
export type ContinueConfigurationPreset = z.infer<
typeof continueConfigurationPresetSchema
>
export const runtimeCustomizationSettingsSchema = z
.object({
opencode: z
.object({
defaultAgent: boundedRuntimeIdentifierSchema.optional()
})
.strict(),
continue: z
.object({
defaultPresetId: assistantIdSchema.optional(),
presets: z
.array(continueConfigurationPresetSchema)
.max(runtimeCustomizationLimits.presets)
})
.strict()
})
.strict()
.superRefine((settings, context) => {
const presetIds = settings.continue.presets.map(
(preset) => preset.id
)
if (new Set(presetIds).size !== presetIds.length) {
context.addIssue({
code: 'custom',
path: ['continue', 'presets'],
message: 'Continue 预设 ID 不得重复'
})
}
if (
settings.continue.defaultPresetId &&
!presetIds.includes(settings.continue.defaultPresetId)
) {
context.addIssue({
code: 'custom',
path: ['continue', 'defaultPresetId'],
message: '默认 Continue 预设不存在'
})
}
})
export type RuntimeCustomizationSettings = z.infer<
typeof runtimeCustomizationSettingsSchema
>
export const defaultRuntimeCustomizationSettings: RuntimeCustomizationSettings =
{
opencode: {},
continue: {
presets: []
}
}
export const runtimeControlSchema = z.discriminatedUnion(
'provider',
[
z
.object({
provider: z.literal('opencode'),
agent: boundedRuntimeIdentifierSchema.optional(),
command: z
.object({
name: boundedRuntimeIdentifierSchema,
arguments: z.string().max(100_000)
})
.strict()
.optional()
})
.strict(),
z
.object({
provider: z.literal('continue'),
presetId: assistantIdSchema.optional()
})
.strict()
]
)
export type RuntimeControl = z.infer<typeof runtimeControlSchema>
export const runtimeNativeSnapshotInputSchema = z
.object({
provider: customizableRuntimeProviderSchema,
profileId: assistantIdSchema.optional(),
projectId: assistantIdSchema.optional()
})
.strict()
export type RuntimeNativeSnapshotInput = z.infer<
typeof runtimeNativeSnapshotInputSchema
>
export const runtimeNativeSkillSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
description: boundedRuntimeDescriptionSchema,
source: z
.enum(['global', 'workspace', 'plugin', 'runtime', 'unknown'])
.default('unknown')
})
.strict()
const runtimeNativeMcpServerSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
status: z.enum([
'connected',
'disabled',
'failed',
'needs-auth',
'unsupported',
'unknown'
]),
detail: z.string().trim().max(500).optional()
})
.strict()
export const runtimeNativePromptSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
description: boundedRuntimeDescriptionSchema,
prompt: z.string().trim().min(1).max(20_000),
source: z.enum([
'runtime',
'mcp',
'configuration',
'preset'
])
})
.strict()
export type RuntimeNativePrompt = z.infer<
typeof runtimeNativePromptSchema
>
export const runtimeNativeRuleSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
description: boundedRuntimeDescriptionSchema,
content: z.string().trim().min(1).max(20_000),
source: z.enum([
'global',
'workspace',
'configuration',
'runtime'
])
})
.strict()
export type RuntimeNativeRule = z.infer<
typeof runtimeNativeRuleSchema
>
const runtimeNativeResourceSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
uri: z.string().trim().min(1).max(2_048),
description: boundedRuntimeDescriptionSchema,
mimeType: z.string().trim().min(1).max(200).optional(),
server: boundedRuntimeLabelSchema.optional()
})
.strict()
const runtimeNativeAgentSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
description: boundedRuntimeDescriptionSchema,
mode: z.enum(['primary', 'subagent', 'all']),
native: z.boolean(),
hidden: z.boolean()
})
.strict()
const runtimeNativeCommandSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
description: boundedRuntimeDescriptionSchema,
source: z.enum(['command', 'mcp', 'skill', 'runtime']),
agent: boundedRuntimeIdentifierSchema.optional()
})
.strict()
const runtimeNativeLspSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
status: z.enum(['connected', 'error', 'not-loaded']),
detail: z.string().trim().max(500).optional()
})
.strict()
const runtimeNativeFormatterSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
enabled: z.boolean(),
extensions: z
.array(z.string().trim().min(1).max(32))
.max(100)
})
.strict()
export const runtimeNativeToolSchema = z
.object({
id: boundedRuntimeIdentifierSchema,
name: boundedRuntimeLabelSchema,
description: boundedRuntimeDescriptionSchema,
kind: z.enum([
'read',
'write',
'shell',
'network',
'agent',
'interaction',
'other'
]),
source: z.enum([
'runtime',
'plugin',
'mcp',
'skill',
'unknown'
]),
ask: z.enum(['allowed', 'blocked', 'conditional']),
execute: z.enum(['allowed', 'blocked', 'conditional'])
})
.strict()
export type RuntimeNativeTool = z.infer<
typeof runtimeNativeToolSchema
>
export const runtimeNativeInventoryStatusSchema = z.enum([
'available',
'partial',
'unavailable',
'connection-only',
'unsupported'
])
export type RuntimeNativeInventoryStatus = z.infer<
typeof runtimeNativeInventoryStatusSchema
>
export const runtimeContextCapabilitySchema = z
.object({
strategy: z.enum([
'native',
'goodbuddy-summary',
'unsupported'
]),
manualCompact: z.boolean(),
detail: z.string().trim().min(1).max(500)
})
.strict()
export type RuntimeContextCapability = z.infer<
typeof runtimeContextCapabilitySchema
>
export const runtimeNativeSnapshotSchema = z
.object({
provider: customizableRuntimeProviderSchema,
available: z.boolean(),
inventoryStatus: runtimeNativeInventoryStatusSchema,
detail: z.string().trim().min(1).max(1_000),
agents: z
.array(runtimeNativeAgentSchema)
.max(runtimeNativeInventoryLimits.agents),
tools: z
.array(runtimeNativeToolSchema)
.max(runtimeNativeInventoryLimits.tools),
toolsSupported: z.boolean(),
commands: z
.array(runtimeNativeCommandSchema)
.max(runtimeNativeInventoryLimits.commands),
lsp: z
.array(runtimeNativeLspSchema)
.max(runtimeNativeInventoryLimits.lsp),
formatters: z
.array(runtimeNativeFormatterSchema)
.max(runtimeNativeInventoryLimits.formatters),
mcpServers: z
.array(runtimeNativeMcpServerSchema)
.max(runtimeNativeInventoryLimits.mcpServers),
skills: z
.array(runtimeNativeSkillSchema)
.max(runtimeNativeInventoryLimits.skills),
rules: z
.array(runtimeNativeRuleSchema)
.max(runtimeNativeInventoryLimits.rules),
prompts: z
.array(runtimeNativePromptSchema)
.max(runtimeNativeInventoryLimits.prompts),
resources: z
.array(runtimeNativeResourceSchema)
.max(runtimeNativeInventoryLimits.resources),
resourcesSupported: z.boolean(),
context: runtimeContextCapabilitySchema
})
.strict()
export type RuntimeNativeSnapshot = z.infer<
typeof runtimeNativeSnapshotSchema
>
export const runtimeConversationCompactInputSchema = z
.object({
requestId: assistantIdSchema,
conversationId: assistantIdSchema,
projectId: assistantIdSchema.optional(),
runtimeSelection: agentRuntimeSelectionSchema,
history: z.array(conversationHistoryMessageSchema),
historyMessageIds: z.array(assistantIdSchema),
contextCompressionState:
conversationContextCompressionStateSchema.optional()
})
.strict()
.superRefine((request, context) => {
if (
request.history.length !== request.historyMessageIds.length
) {
context.addIssue({
code: 'custom',
path: ['historyMessageIds'],
message: '会话历史消息 ID 必须与历史消息一一对应'
})
}
if (
new Set(request.historyMessageIds).size !==
request.historyMessageIds.length
) {
context.addIssue({
code: 'custom',
path: ['historyMessageIds'],
message: '会话历史消息 ID 不得重复'
})
}
})
export type RuntimeConversationCompactInput = z.infer<
typeof runtimeConversationCompactInputSchema
>
export const runtimeConversationCompactResultSchema = z
.object({
provider: z.enum(['opencode', 'continue', 'model']),
strategy: z.enum(['native', 'goodbuddy-summary']),
compacted: z.boolean(),
detail: z.string().trim().min(1).max(500),
contextCompressionState:
conversationContextCompressionStateSchema.optional()
})
.strict()
export type RuntimeConversationCompactResult = z.infer<
typeof runtimeConversationCompactResultSchema
>