feat(subagent): add current-turn interrupt RPC

ctx.subagents.interrupt() stops one live continuable child's current turn
via Agent.cancel(cause, { keepInbox: true }) under either a human durable
parent address or an exact live ancestor Agent. Fire-and-return: admission
is synchronous, quiescence is not awaited. Pending inbox work, the
Activation, and published descendants are preserved; only a later waking
send resumes the parked FIFO queue. Absent, one-shot, and disposing
targets are accepted no-ops.

The new Host RPC subagent.interrupt calls only that primitive with user
authority — no catalog, history, persistence, or parent-registry lookup —
so a live child stays stoppable while its parent Agent is offline.

Refs #1535
This commit is contained in:
Hypatia May
2026-08-06 11:59:19 +08:00
committed by Tianyi Cui
parent 5f9456c24f
commit 66a21a38b1
27 changed files with 613 additions and 17 deletions
+2 -1
View File
@@ -42,7 +42,8 @@ export type {
} from './sessions.ts'
export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
export type {
SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, SubagentsApi,
SubagentAddress, SubagentCatalog, SubagentInterruptReceipt, SubagentListEntry,
SubagentPromptReceipt, SubagentsApi,
} from './subagents.ts'
export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts'
export type { CommandsApi, CommandDescriptor } from './commands.ts'
@@ -36,6 +36,7 @@ export interface RpcMethodMap {
'subagent.list': SubagentsApi['list']
'subagent.history': SubagentsApi['history']
'subagent.prompt': SubagentsApi['prompt']
'subagent.interrupt': SubagentsApi['interrupt']
'host.describe': HostApi['describe']
'host.pickDirectory': HostApi['pickDirectory']
'host.listDirectory': HostApi['listDirectory']
@@ -69,6 +69,18 @@ export const subagentPromptRequestSchema = z.object({
content: z.array(contentBlockSchema),
}) as unknown as z.ZodType<RequestPayload<'subagent.prompt'>>
/** subagent.interrupt request payload. */
export const subagentInterruptRequestSchema = z.object({
parentSessionId: sessionIdSchema,
childSessionId: sessionIdSchema,
mode: z.literal('continuable'),
}) satisfies z.ZodType<Wire<RequestPayload<'subagent.interrupt'>>>
/** subagent.interrupt response value. */
export const subagentInterruptValueSchema = z.object({
accepted: z.literal(true),
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.interrupt'>>>
const messageIdSchema = z.string() as unknown as z.ZodType<MessageId>
/** subagent.prompt response value. */
@@ -40,6 +40,11 @@ export interface SubagentPromptReceipt {
messageId: MessageId
}
/** Uniform acknowledgement that one interrupt request was admitted. */
export interface SubagentInterruptReceipt {
accepted: true
}
/** Durable parent/child address that selects subagent transport in the client. */
export type SubagentAddress =
& {
@@ -94,4 +99,17 @@ export interface SubagentsApi {
>,
signal: AbortSignal,
): Promise<RpcResponse<SubagentPromptReceipt>>
/**
* Interrupts a live continuable child's current turn under the address's
* durable direct-parent authority, without requiring a live parent Agent,
* consulting the catalog, or resuming anything. Fire-and-return: `accepted`
* acknowledges the admitted cancel signal, not target quiescence, so the
* child may remain visibly running briefly. Queued follow-ups are kept and
* parked; an absent, idle, or already-completed target is likewise
* `accepted`.
*/
interrupt(
request: RpcRequest<Extract<SubagentAddress, { mode: 'continuable' }>>,
): Promise<RpcResponse<SubagentInterruptReceipt>>
}