import type { ToolSpec } from '@shared/types' import type { ConversationToolContext, ConversationToolResult } from '../agent/conversation-tools' import { listSkills, buildSkillContext, loadSkillContext, readSkillResource } from './skills' import { snapshotMcpTools } from './mcp' import { expertManagerDocument, expertManagerAdapter, expertManagerResource, expertPackageOperation, expertTurnContext } from './experts' import { agentMode, agentModeInstructions, modeToolDenial, modeToolSpecs } from '../agent/mode-policy' import type { AgentMode } from '@shared/conversation' export interface CapabilityToolSnapshot { specs: ToolSpec[] /** Host-enforced subsets, e.g. a bounded child worker. Applied to built-in tools too. */ filterSpecs?(specs: ToolSpec[]): ToolSpec[] aroundExecute?(name: string, args: Record, ctx: ConversationToolContext, execute: () => Promise): Promise metadata?(name: string): Record | undefined execute(name: string, args: Record, ctx: ConversationToolContext): Promise } export interface CapabilitySnapshot extends CapabilityToolSnapshot { context: string references: Array<{ id: string; name: string; uri: string }> assistantDisplay?: import('@shared/capabilities').ExpertBinding } /** A turn retains stable capability IDs and MCP connection generations. */ export async function createCapabilitySnapshot(ids: string[], cwd: string, options: { sessionId?: string; expertId?: string | null; agentMode?: AgentMode } = {}): Promise { const mode = agentMode(options.agentMode) if (!Array.isArray(ids) || ids.length > 32 || ids.some(id => typeof id !== 'string')) throw new Error('技能引用无效') const selected = [...new Set(ids)] const installed = await listSkills(cwd) const references = selected.map(id => { if (id === 'builtin:expert-manager') return { id, name: 'expert-manager', uri: 'skill://expert-manager' } const skill = installed.find(item => item.id === id) if (!skill?.enabled || !skill.userInvocable) throw new Error('引用的技能已禁用或不可手动调用') return { id, name: skill.name, uri: `skill://${encodeURIComponent(skill.name)}` } }) const managerId = 'builtin:expert-manager' let managerLoaded = selected.includes(managerId) const managerContext = (): string => `\n${expertManagerDocument().content}\n${expertManagerAdapter(options.sessionId ?? '')}` const skillContext = [await buildSkillContext(selected.filter(id => id !== managerId), cwd), managerLoaded ? managerContext() : ''].filter(Boolean).join('\n\n') const available = installed.filter(item => item.enabled && !item.disableModelInvocation) const allowedIds = new Set([...selected, ...available.map(item => item.id), managerId]) const mcp = await snapshotMcpTools() // Snapshot first: a concurrent MCP reconfiguration invalidates these generations, // so dependency checks can never authorize a newly substituted connection. const activeExpert = options.expertId ? await expertTurnContext(options.expertId) : undefined const specs: ToolSpec[] = [...mcp.specs] if (options.sessionId) specs.push({ type: 'function', function: { name: 'ycode_expert_package', description: '创建或编辑专家包:read 读取当前包;write 增量保存包内文件;validate 校验;register 原子注册到我的专家。冲突后使用 read(refresh:true) 重新读取最新包。必须先加载 expert-manager 技能。', parameters: { type: 'object', properties: { id: { type: 'string' }, action: { type: 'string', enum: ['read', 'write', 'validate', 'register'] }, refresh: { type: 'boolean' }, files: { type: 'object', additionalProperties: { anyOf: [{ type: 'string' }, { type: 'null' }] } } }, required: ['id', 'action'] } } }) if (activeExpert) specs.push({ type: 'function', function: { name: 'ycode_expert_resource', description: '读取当前激活专家包内的 UTF-8 文本资源。', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } }) if (allowedIds.size) { specs.push({ type: 'function', function: { name: 'ycode_load_skill', description: '加载可用技能的完整指令。开始对应任务前先加载技能,遵循其流程。', parameters: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } } }) specs.push({ type: 'function', function: { name: 'ycode_read_skill_resource', description: '读取技能包中的文本资源。path 是包内相对路径。', parameters: { type: 'object', properties: { id: { type: 'string' }, path: { type: 'string' } }, required: ['id', 'path'] } } }) } const names = new Set(mcp.specs.map(spec => spec.function.name)) return { specs: modeToolSpecs(specs, mode), references, assistantDisplay: activeExpert?.display, metadata: name => { const identity = mcp.describe(name); return identity ? { capability: 'mcp', ...identity } : undefined }, context: [activeExpert?.context ?? (options.expertId === null ? '当前会话已移除专家,后续回复使用默认助手身份,不再沿用之前专家的角色指令。' : ''), skillContext, `可自动调用的技能(先用 ycode_load_skill 加载):\n${[...available.map(item => ({ id: item.id, name: item.name, description: item.description })), { id: managerId, name: 'expert-manager', description: '创建、转换、修改、校验本地专家包。' }].map(item => JSON.stringify(item)).join('\n')}`, agentModeInstructions(mode)].filter(Boolean).join('\n\n'), async execute(name, args, ctx) { if (ctx.signal.aborted) throw Object.assign(new Error('回合已取消'), { name: 'AbortError' }) const denial = modeToolDenial(mode, name) if (denial) return denial if (name === 'ycode_expert_package') { if (!managerLoaded || !options.sessionId) return { ok: false, content: '请先加载完整 expert-manager 技能' } try { return { ok: true, content: JSON.stringify(await expertPackageOperation(options.sessionId, args)), metadata: { capability: 'expert-package', action: args.action, expertId: args.id } } } catch (e) { return { ok: false, content: e instanceof Error ? e.message : String(e) } } } if (name === 'ycode_expert_resource' && options.expertId) { const resource = typeof args.path === 'string' ? args.path.replace(/^\.\//, '') : '' const content = activeExpert && Object.hasOwn(activeExpert.resources, resource) ? activeExpert.resources[resource] : undefined return typeof content !== 'string' ? { ok: false, content: '当前专家包中没有此文本资源' } : { ok: true, content } } if (name === 'ycode_load_skill' || name === 'ycode_read_skill_resource') { if (typeof args.id !== 'string' || !allowedIds.has(args.id)) return { ok: false, content: '技能不在当前回合可用范围内' } if (args.id === managerId) { if (name === 'ycode_load_skill') { managerLoaded = true; return { ok: true, content: managerContext() } } return { ok: true, content: expertManagerResource(String(args.path ?? '')) } } const content = name === 'ycode_load_skill' ? await loadSkillContext(args.id, cwd) : await readSkillResource(args.id, String(args.path ?? ''), cwd) return { ok: true, content } } if (!names.has(name)) return undefined if (ctx.permissionMode !== 'full-access') { if (!ctx.interactions) return { ok: false, content: 'MCP 调用需要用户确认' } const identity = mcp.describe(name) const decision = await ctx.interactions.approval(ctx.toolCallId, identity ? `调用 ${identity.serverName} 的 ${identity.toolName} 工具` : `调用连接器工具 ${name}`, [ { optionId: 'allow-once', name: '允许一次', kind: 'allow_once' }, { optionId: 'reject-once', name: '拒绝', kind: 'reject_once' } ]) if (decision.optionId !== 'allow-once') return { ok: false, content: '用户拒绝了连接器调用' } } const result = await mcp.execute(name, args, ctx.signal) return { ...result, metadata: { capability: 'mcp', ...mcp.describe(name) } } } } }