refactor(host)!: retire the skill.invoke RPC for the gesture boundary

Invocation is an ordinary session.prompt again: the pre-step gesture
boundary makes it deterministic host-side for every front end, so the
dedicated RPC (handler, wire schema, error codes, client face, fixtures)
and ui-skill's claim machinery are net deletions. The menu keeps decision
21 exactly — a pick lands literal /name text — plus the user-only marker
from skill.list's modelInvocable flag.
This commit is contained in:
Yichen Jiang
2026-08-08 13:15:52 +08:00
parent c08fa27e5c
commit 0d53752c49
43 changed files with 143 additions and 663 deletions
@@ -50,7 +50,6 @@ export interface RpcMethodMap {
'command.list': CommandsApi['list']
'command.execute': CommandsApi['execute']
'skill.list': SkillsApi['list']
'skill.invoke': SkillsApi['invoke']
'goal.create': GoalsApi['create']
'goal.edit': GoalsApi['edit']
'goal.pause': GoalsApi['pause']
@@ -51,8 +51,6 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('steer-unavailable'), message: z.string(), details: z.object({ itemId: z.string() }) }),
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('skill-not-found'), message: z.string(), details: z.object({ name: z.string() }) }),
z.object({ code: z.literal('skill-not-invocable'), message: z.string(), details: z.object({ name: z.string() }) }),
z.object({ code: z.literal('settings-rejected'), message: z.string(), details: z.object({ ns: z.string() }) }),
z.object({ code: z.literal('settings-not-exposed'), message: z.string(), details: z.object({ ns: z.string() }) }),
z.object({ code: z.literal('settings-conflict'), message: z.string(), details: z.object({ ns: z.string(), expected: z.number(), actual: z.number() }) }),
-4
View File
@@ -51,10 +51,6 @@ export interface RpcErrorDetailsMap {
'command-error': {}
/** A leading-/ prompt named no registered command; the message names the token. */
'unknown-command': {}
/** A skill invocation named no skill in the session's workspace (unknown or ill-formed name). */
'skill-not-found': { name: string }
/** A skill invocation named a skill whose policy forbids user invocation. */
'skill-not-invocable': { name: string }
/**
* A settings write was refused (schema validation, unknown namespace,
* read-only provider, or storage failure); the message is the seam's text.
@@ -26,18 +26,3 @@ export const skillListRequestSchema = z.object({
export const skillListValueSchema = z.object({
skills: z.array(skillEntrySchema),
}) satisfies z.ZodType<Wire<ResponseValue<'skill.list'>>>
/**
* skill.invoke request payload. `text` is the user's trailing message; a
* blank one stays off the wire (the boundary, not client courtesy, refuses it).
*/
export const skillInvokeRequestSchema = z.object({
sessionId: sessionIdSchema,
name: z.string().min(1),
text: z.string().min(1).optional(),
}) satisfies z.ZodType<Wire<RequestPayload<'skill.invoke'>>>
/** skill.invoke response value. */
export const skillInvokeValueSchema = z.object({
accepted: z.literal(true),
}) satisfies z.ZodType<Wire<ResponseValue<'skill.invoke'>>>
+7 -15
View File
@@ -20,22 +20,14 @@ export interface SkillEntry {
readonly modelInvocable: boolean
}
/** Skill-domain unary methods (the map keys skill.* of RpcMethodMap). */
/**
* Skill-domain unary methods (the map key skill.* of RpcMethodMap). Listing
* is the domain's only RPC: invocation itself is a plain `session.prompt`
* whose leading `/name` token the host recognizes at the pre-step boundary
* (`dsh-tool-skill` injects the rendered body there), so every client shares
* one deterministic path with no dedicated invocation wire.
*/
export interface SkillsApi {
/** Lists the user-invocable skill catalog for the session's project. */
list(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ skills: readonly SkillEntry[] }>>
/**
* Injects one user-invocable skill into the addressed agent as a user-role
* message (the canonical `<skill_content>` rendering, with `text` appended
* when present) and starts a turn. The host enforces user-invocation policy
* here — on the discovery summary and again on the loaded definition, so a
* catalog change between the two lookups cannot slip a user-disabled body
* through — a model-only or unknown name is refused regardless of what a
* client menu offered. The carrier's request signal aborts the skill
* lookup and refuses injection once the caller has given up (`cancelled`).
* Session-backed subagents reject with `agent-busy`.
*/
invoke(request: RpcRequest<{ sessionId: SessionId; name: string; text?: string }>, signal: AbortSignal):
Promise<RpcResponse<{ accepted: true }>>
}