fix: guard human interaction by runtime ownership

This commit is contained in:
Tianyi Cui
2026-08-08 15:30:08 +08:00
parent 408de0f588
commit dc64c5f1c2
27 changed files with 935 additions and 81 deletions
@@ -4,7 +4,7 @@ import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { RUN_CODE_NAME, defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { Session, SessionId, type UserMessage } from '@deepseek-ai/dsh-session'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { createScope } from '@deepseek-ai/dsh-scope'
import UserInteractionService, {
UserInteractionError, type AskUserQuestionRequest,
@@ -25,7 +25,11 @@ const PLAN_CONFIG = { section: TEST_PLAN_SECTION } satisfies PlanModeConfig
* and the following `step/start` session event used by the loop.
*/
async function agentWithSession(ctx: Context, id = 'agent-1', { active }: { active?: boolean } = {}): Promise<Agent & { session: Session }> {
async function agentWithSession(
ctx: Context,
id = 'agent-1',
{ active, owner }: { active?: boolean; owner?: Agent } = {},
): Promise<Agent & { session: Session }> {
// A live store session when a store is mounted (the command executor logs
// lifecycle events through it); bare otherwise (fold/tool-only benches).
const session = Session.create(SessionId(id))
@@ -44,8 +48,15 @@ async function agentWithSession(ctx: Context, id = 'agent-1', { active }: { acti
;(agent as { ctx?: Context }).ctx = scoped
// Seeded plan state lands before the creation announcement, matching resume.
if (active !== undefined) session.append('plan/mode', { active })
// The loop announces creation after publication.
ctx.emit('agent/created', { agent })
// The loop publishes through the live registry when it is composed; narrow
// fold-only benches retain the direct lifecycle event used before it exists.
const agents = ctx.get('agents')
if (agents === undefined) {
ctx.emit('agent/created', { agent })
} else {
agents.enter(agent, owner)
agents.announce(agent)
}
return agent
}
@@ -653,6 +664,7 @@ describe('/plan', () => {
describe('exit_plan_mode', () => {
async function setupWithReview(answer?: { selected: string[]; custom?: string }) {
const ctx = await setup()
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
const asked: AskUserQuestionRequest[] = []
if (answer !== undefined) {
@@ -730,6 +742,26 @@ describe('exit_plan_mode', () => {
expect(foldPlanMode(agent.session.events)).toBe(true)
})
it('rejects review from a runtime-owned agent with consumer-neutral guidance', async () => {
const ctx = await setup()
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
const ask = vi.fn(async () => ({ answers: [{ id: 'plan-review', selected: ['Approve'] }] }))
ctx.userInteraction.registerProvider({ ask })
const root = await agentWithSession(ctx, 'review-root')
const child = await agentWithSession(ctx, 'review-child', { active: true, owner: root })
const result = await callExit(ctx, child)
expect(result.isError).toBe(true)
expect(result.content).toEqual([{
type: 'text',
text: "Error: human interaction is unavailable while the calling agent is owned by another live agent; include the unresolved question or decision in the child agent's final result",
}])
expect(ask).not.toHaveBeenCalled()
expect(foldPlanMode(child.session.events)).toBe(true)
})
it('approve: records the boundary-applied switch and confirms (the fold flips at the flush)', async () => {
const { ctx, agent, asked } = await setupWithReview({ selected: ['Approve'] })
const result = await callExit(ctx, agent)
@@ -765,6 +797,7 @@ describe('exit_plan_mode', () => {
await ctx.plugin(ToolRegistry, { mode: 'code' })
await ctx.plugin(ExitRuntime)
await ctx.plugin(PlanModeService, PLAN_CONFIG)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
const asked: AskUserQuestionRequest[] = []
ctx.userInteraction.registerProvider({
@@ -939,6 +972,7 @@ describe('exit_plan_mode', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
const fiber = await ctx.plugin(PlanModeService, PLAN_CONFIG)
await ctx.plugin(AgentRegistry)
await ctx.plugin(UserInteractionService)
let answer!: (value: { answers: { id: string; selected: string[] }[] }) => void
ctx.userInteraction.registerProvider({