feat(host): user-invocable skill listing and skill.invoke injection RPC
skill.list now serves every user-invocable skill and carries modelInvocable so menus can mark user-only entries; the old model-and-user intersection hid disable-model-invocation skills from their only legitimate entry point (issue #1470). skill.invoke enforces user-invocation policy at the host boundary, renders the canonical <skill_content> body, and injects it as a user-role message carrying the skill-invocation source before starting a turn. The connection fixture mirrors both faces for client tests.
This commit is contained in:
@@ -2449,10 +2449,28 @@ function createFixtureWorld(options: FixtureOptions): FixtureWorld {
|
||||
if (missing !== undefined) return missing
|
||||
return ok(request, {
|
||||
skills: [
|
||||
{ name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收' },
|
||||
{ name: 'fixture-demo', description: 'fixture 技能样本', whenToUse: '仅供 UI 目录渲染验收', modelInvocable: true },
|
||||
{ name: 'fixture-user-only', description: 'fixture 仅用户技能样本', modelInvocable: false },
|
||||
],
|
||||
})
|
||||
},
|
||||
invoke: (request) => {
|
||||
const missing = requireSession(request)
|
||||
if (missing !== undefined) return missing
|
||||
const { sessionId, name, text: args } = request.payload
|
||||
const body = `<skill_content name="${name}">\n<skill_resources>\nBase directory for this skill: /fixture/skills/${name}\n</skill_resources>\n\n<skill_instructions>\nFixture ${name} instructions.\n</skill_instructions>\n</skill_content>`
|
||||
// Mirror the host: injection is a user-role message carrying the
|
||||
// skill-invocation source, immediately visible in the transcript.
|
||||
// The client program cannot see the host-side MessageSourceMap merge
|
||||
// (sources are opaque wire JSON to the UI), so the fixture stamps the
|
||||
// durable shape through the same assertion the projections read back.
|
||||
const source = { kind: 'skill-invocation', name, ...args === undefined ? {} : { args } } as unknown as MessageSource
|
||||
append(sessionId, {
|
||||
type: 'user/message', surfaceOp: 'append',
|
||||
data: userMessage(text(args === undefined ? body : `${body}\n\n${args}`), source),
|
||||
})
|
||||
return ok(request, { accepted: true as const })
|
||||
},
|
||||
},
|
||||
goals: {
|
||||
// Compatibility face only: old API Proxy payloads and acknowledgements
|
||||
@@ -2761,6 +2779,7 @@ export class FixtureApiClient extends AbstractApiClient {
|
||||
case 'command.list': return this.api.commands.list(request)
|
||||
case 'command.execute': return this.api.commands.execute(request, signal)
|
||||
case 'skill.list': return this.api.skills.list(request)
|
||||
case 'skill.invoke': return this.api.skills.invoke(request)
|
||||
case 'goal.create': return this.api.goals.create(request)
|
||||
case 'goal.edit': return this.api.goals.edit(request)
|
||||
case 'goal.pause': return this.api.goals.pause(request)
|
||||
|
||||
Reference in New Issue
Block a user