Merge pull request #2809 from code-yeongyu/fix/2330-recursive-subagent-spawn
fix(task): preserve restricted agent tools in sync continuation
This commit is contained in:
@@ -464,4 +464,212 @@ describe("executeSyncContinuation - toast cleanup error paths", () => {
|
|||||||
expect(result).toContain("session_id: ses_test_12345678")
|
expect(result).toContain("session_id: ses_test_12345678")
|
||||||
expect(result).not.toContain("subagent:")
|
expect(result).not.toContain("subagent:")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("preserves restricted tool permissions for resumed explore sessions", async () => {
|
||||||
|
//#given - a resumed explore session should not regain delegation tools
|
||||||
|
const promptAsyncCalls: Array<{ path: { id: string }; body: Record<string, unknown> }> = []
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
id: "msg_002",
|
||||||
|
role: "assistant",
|
||||||
|
time: { created: 2000 },
|
||||||
|
finish: "end_turn",
|
||||||
|
agent: "explore",
|
||||||
|
},
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||||
|
promptAsyncCalls.push(input)
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
pollSyncSession: async () => null,
|
||||||
|
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
syncPollTimeoutMs: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "continue working",
|
||||||
|
description: "resume explore task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptAsyncCalls).toHaveLength(1)
|
||||||
|
expect(promptAsyncCalls[0]?.body.tools).toEqual({
|
||||||
|
task: false,
|
||||||
|
call_omo_agent: false,
|
||||||
|
question: false,
|
||||||
|
write: false,
|
||||||
|
edit: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("preserves restricted tool permissions for resumed librarian sessions", async () => {
|
||||||
|
//#given - a resumed librarian session should stay read-only for delegation tools
|
||||||
|
const promptAsyncCalls: Array<{ path: { id: string }; body: Record<string, unknown> }> = []
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
id: "msg_002",
|
||||||
|
role: "assistant",
|
||||||
|
time: { created: 2000 },
|
||||||
|
finish: "end_turn",
|
||||||
|
agent: "librarian",
|
||||||
|
},
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||||
|
promptAsyncCalls.push(input)
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
pollSyncSession: async () => null,
|
||||||
|
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
syncPollTimeoutMs: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "continue researching",
|
||||||
|
description: "resume librarian task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptAsyncCalls).toHaveLength(1)
|
||||||
|
expect(promptAsyncCalls[0]?.body.tools).toEqual({
|
||||||
|
task: false,
|
||||||
|
call_omo_agent: false,
|
||||||
|
question: false,
|
||||||
|
write: false,
|
||||||
|
edit: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("keeps plan-family task delegation available during sync continuation", async () => {
|
||||||
|
//#given - a resumed plan-family session should keep its intended task capability
|
||||||
|
const promptAsyncCalls: Array<{ path: { id: string }; body: Record<string, unknown> }> = []
|
||||||
|
const mockClient = {
|
||||||
|
session: {
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{ info: { id: "msg_001", role: "user", time: { created: 1000 } } },
|
||||||
|
{
|
||||||
|
info: {
|
||||||
|
id: "msg_002",
|
||||||
|
role: "assistant",
|
||||||
|
time: { created: 2000 },
|
||||||
|
finish: "end_turn",
|
||||||
|
agent: "prometheus",
|
||||||
|
},
|
||||||
|
parts: [{ type: "text", text: "Response" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async (input: { path: { id: string }; body: Record<string, unknown> }) => {
|
||||||
|
promptAsyncCalls.push(input)
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
status: async () => ({
|
||||||
|
data: { ses_test: { type: "idle" } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeSyncContinuation } = require("./sync-continuation")
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
pollSyncSession: async () => null,
|
||||||
|
fetchSyncResult: async () => ({ ok: true as const, textContent: "Result" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
sessionID: "parent-session",
|
||||||
|
callID: "call-123",
|
||||||
|
metadata: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockExecutorCtx = {
|
||||||
|
client: mockClient,
|
||||||
|
syncPollTimeoutMs: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
session_id: "ses_test_12345678",
|
||||||
|
prompt: "continue planning",
|
||||||
|
description: "resume plan task",
|
||||||
|
load_skills: [],
|
||||||
|
run_in_background: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
//#when
|
||||||
|
await executeSyncContinuation(args, mockCtx, mockExecutorCtx, deps)
|
||||||
|
|
||||||
|
//#then
|
||||||
|
expect(promptAsyncCalls).toHaveLength(1)
|
||||||
|
expect(promptAsyncCalls[0]?.body.tools).toEqual({
|
||||||
|
task: true,
|
||||||
|
call_omo_agent: true,
|
||||||
|
question: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -85,10 +85,10 @@ export async function executeSyncContinuation(
|
|||||||
const allowTask = isPlanFamily(resumeAgent)
|
const allowTask = isPlanFamily(resumeAgent)
|
||||||
const effectivePrompt = buildTaskPrompt(args.prompt, resumeAgent)
|
const effectivePrompt = buildTaskPrompt(args.prompt, resumeAgent)
|
||||||
const tools = {
|
const tools = {
|
||||||
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
|
||||||
task: allowTask,
|
task: allowTask,
|
||||||
call_omo_agent: true,
|
call_omo_agent: true,
|
||||||
question: false,
|
question: false,
|
||||||
|
...(resumeAgent ? getAgentToolRestrictions(resumeAgent) : {}),
|
||||||
}
|
}
|
||||||
setSessionTools(args.session_id!, tools)
|
setSessionTools(args.session_id!, tools)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user