diff --git a/apps/electron/default-skills/agent-collaboration/SKILL.md b/apps/electron/default-skills/agent-collaboration/SKILL.md index 5dff18c57..4653f4c91 100644 --- a/apps/electron/default-skills/agent-collaboration/SKILL.md +++ b/apps/electron/default-skills/agent-collaboration/SKILL.md @@ -2,7 +2,7 @@ name: agent-collaboration description: Proma 协作子 Agent Skill。当需要并行探索多个方向(多样性探索)、对抗性审查验证已有方案、或多个长耗时独立任务需要真实可见的子会话时触发。用于判断是否以及如何调用 Proma 内置 collaboration 工具创建协作子会话。简单搜索、短调研、单文件修改、一次性代码审查由父会话直接使用普通工具完成。 group: proma -version: "1.1.1" +version: "1.1.2" --- # Proma Agent Collaboration @@ -20,6 +20,8 @@ Proma 已提供内置 `collaboration` MCP 工具。你必须通过这些工具 - `collaboration.list_delegations`:查看当前父会话创建的子会话状态。 - `collaboration.get_delegation_results`:按委派 ID 读取一个或多个子会话结果摘要。 - `collaboration.stop_delegation` / `collaboration.stop_delegations`:停止一个或一批子会话。 +- `collaboration.continue_delegation`:向已结束的子会话追加指令,并等待本轮完成后返回。 +- `collaboration.continue_delegation_async`:向已结束的子会话追加指令并立即返回;后续用 `wait_for_delegations` 或 `get_delegation_results` 收敛结果。 ## 先判断用哪种能力 @@ -121,8 +123,9 @@ Proma 已提供内置 `collaboration` MCP 工具。你必须通过这些工具 - 如果父会话还有独立主线可推进,先继续处理自己的工作,不要因为已经派发子会话就空等。 - 如果需要快速校准方向,用 `mode=any` / `minCompleted` 先收敛一部分结果,再决定父会话继续做什么。 5. 调用 `collaboration.wait_for_delegations` 收敛结果;几十个并行任务可以先用 `mode=any` 等一部分完成,再决定是否继续等待或停止剩余任务。非阻塞推进时,可以先 `list_delegations`,再用 `get_delegation_results` 按 ID 拉取结果。 -6. 整合子会话发现,明确哪些结论来自哪个子会话。 -7. 如某个子会话或一批子会话卡住、重复或方向错误,用 `collaboration.stop_delegation` / `collaboration.stop_delegations` 停止。 +6. 如需让已结束的子会话继续下一轮,阻塞等待结果时用 `continue_delegation`;父会话还有独立工作可推进时用 `continue_delegation_async`,并在稍后显式等待或读取结果。 +7. 整合子会话发现,明确哪些结论来自哪个子会话。 +8. 如某个子会话或一批子会话卡住、重复或方向错误,用 `collaboration.stop_delegation` / `collaboration.stop_delegations` 停止。 ## 委派 task 写法 diff --git a/apps/electron/src/main/lib/agent-collaboration-tools.test.ts b/apps/electron/src/main/lib/agent-collaboration-tools.test.ts new file mode 100644 index 000000000..d5bde2ac7 --- /dev/null +++ b/apps/electron/src/main/lib/agent-collaboration-tools.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, mock, test } from 'bun:test' + +const noop = (): void => {} +const electronStub = { + app: { + getPath: () => '/tmp/proma-collaboration-test', + getAppPath: () => '/tmp/proma-collaboration-test', + isPackaged: false, + on: noop, + }, + BrowserWindow: class BrowserWindow {}, + clipboard: {}, + dialog: {}, + globalShortcut: {}, + ipcMain: {}, + nativeImage: {}, + nativeTheme: {}, + net: {}, + powerMonitor: {}, + powerSaveBlocker: {}, + safeStorage: {}, + screen: {}, + shell: {}, + systemPreferences: {}, +} + +mock.module('electron', () => electronStub) + +const collaborationContext = { + sessionId: 'parent-session', + channelId: 'channel-id', + workspaceId: 'workspace-id', +} as const + +describe('协作委派续跑工具注册', () => { + test('Claude runtime 同时保留同步工具并暴露异步工具', async () => { + const { injectAgentCollaborationMcpServer } = await import('./agent-collaboration-tools') + const sdk = { + tool(name: string): { name: string } { + return { name } + }, + createSdkMcpServer(input: { tools: Array<{ name: string }> }): { tools: Array<{ name: string }> } { + return input + }, + } + const servers: Record> = {} + + await injectAgentCollaborationMcpServer( + sdk as unknown as typeof import('@anthropic-ai/claude-agent-sdk'), + servers, + collaborationContext, + ) + + const server = servers.collaboration as { tools: Array<{ name: string }> } + const names = server.tools.map((tool) => tool.name) + expect(names).toContain('continue_delegation') + expect(names).toContain('continue_delegation_async') + }) + + test('Pi runtime 同时保留同步工具并暴露异步工具', async () => { + const { buildPiCollaborationTools } = await import('./agent-collaboration-tools') + const sdk = { + defineTool(tool: T): T { + return tool + }, + } + + const tools = buildPiCollaborationTools( + sdk as unknown as typeof import('@earendil-works/pi-coding-agent'), + collaborationContext, + ) as Array<{ name: string }> + const names = tools.map((tool) => tool.name) + + expect(names).toContain('mcp__collaboration__continue_delegation') + expect(names).toContain('mcp__collaboration__continue_delegation_async') + }) +}) diff --git a/apps/electron/src/main/lib/agent-collaboration-tools.ts b/apps/electron/src/main/lib/agent-collaboration-tools.ts index 42e5178b2..342f19dfc 100644 --- a/apps/electron/src/main/lib/agent-collaboration-tools.ts +++ b/apps/electron/src/main/lib/agent-collaboration-tools.ts @@ -85,6 +85,7 @@ const delegations = new Map() // Pi 的 provider/retry 流可能重放同一个 tool call;委派会创建真实会话,必须幂等。 const piDelegateAgentCalls = createToolCallIdempotencyCache() const piDelegateAgentsCalls = createToolCallIdempotencyCache() +const piContinueDelegationAsyncCalls = createToolCallIdempotencyCache() // ===== 阻塞事件追踪(Level 1: Blocked Event Bubbling) ===== @@ -732,6 +733,65 @@ function startDelegation( return { record, effectivePermissionMode: permissionMode, effectiveModelId } } +/** + * 在已有协作子会话上启动下一轮指令。 + * + * 这里只负责恢复状态并启动 headless runner,不等待本轮结束;调用方可按需 + * 立即返回,或继续等待 record.completion,从而让同步/异步工具共享同一语义。 + */ +function startDelegationContinuation( + ctx: CollaborationToolContext, + delegationId: string, + message: string, +): DelegationRecord { + const record = getDelegationRecordForContinuation(ctx, delegationId) + if (!record) throw new Error(`未找到当前会话下的委派: ${delegationId}`) + if (record.status === 'running') { + throw new Error(`委派正在运行中,无法追加指令。请先等待完成或停止后再继续: ${delegationId}`) + } + + record.status = 'running' + record.error = undefined + record.resultSummary = undefined + record.completedAt = undefined + const completionHandle = createDelegationCompletion() + record.completion = completionHandle.completion + record.resolveCompletion = completionHandle.resolveCompletion + + updateAgentSessionMeta(record.childSessionId, { delegationStatus: 'running' }) + + runRegisteredHeadlessAgent( + { + sessionId: record.childSessionId, + userMessage: message, + channelId: record.channelId, + modelId: record.modelId, + workspaceId: ctx.workspaceId, + permissionModeOverride: record.permissionMode, + triggeredBy: 'delegation', + startedAt: Date.now(), + }, + { + source: 'delegation', + onError: (error) => { + markDelegationFinished(record, 'failed', { error }) + }, + onComplete: (messages) => { + if (record.status !== 'running') return + const resultSummary = summarizeChildResult(record.childSessionId, messages) + markDelegationFinished(record, 'completed', { resultSummary }) + }, + onTitleUpdated: () => {}, + }, + ).catch((error: unknown) => { + markDelegationFinished(record, 'failed', { + error: error instanceof Error ? error.message : '未知错误', + }) + }) + + return record +} + function buildCollaborationSchemas(z: ZodModule['z']) { const nonBlankString = z.string().trim().min(1) const role = z.enum(['explore', 'research', 'implement', 'review', 'custom']) @@ -1005,54 +1065,10 @@ export async function injectAgentCollaborationMcpServer( ), sdk.tool( 'continue_delegation', - '向已完成、已失败、已取消或已中断的协作子会话追加后续指令。子会话保留完整上下文继续执行。适合多轮协作场景:先让子 Agent 完成第一步,审查结果后继续下一步。', + '向已完成、已失败、已取消或已中断的协作子会话追加后续指令。子会话保留完整上下文继续执行。适合多轮协作场景:先让子 Agent 完成第一步,审查结果后继续下一步。此工具会等待本轮完成;如需立即返回,请使用 continue_delegation_async。', schemas.continueD, async (args) => { - const record = getDelegationRecordForContinuation(ctx, args.delegationId) - if (!record) throw new Error(`未找到当前会话下的委派: ${args.delegationId}`) - if (record.status === 'running') { - throw new Error(`委派正在运行中,无法追加指令。请先等待完成或停止后再继续: ${args.delegationId}`) - } - - record.status = 'running' - record.error = undefined - record.resultSummary = undefined - record.completedAt = undefined - const completionHandle = createDelegationCompletion() - record.completion = completionHandle.completion - record.resolveCompletion = completionHandle.resolveCompletion - - updateAgentSessionMeta(record.childSessionId, { delegationStatus: 'running' }) - - runRegisteredHeadlessAgent( - { - sessionId: record.childSessionId, - userMessage: args.message, - channelId: record.channelId, - modelId: record.modelId, - workspaceId: ctx.workspaceId, - permissionModeOverride: record.permissionMode, - triggeredBy: 'delegation', - startedAt: Date.now(), - }, - { - source: 'delegation', - onError: (error) => { - markDelegationFinished(record, 'failed', { error }) - }, - onComplete: (messages) => { - if (record.status !== 'running') return - const resultSummary = summarizeChildResult(record.childSessionId, messages) - markDelegationFinished(record, 'completed', { resultSummary }) - }, - onTitleUpdated: () => {}, - }, - ).catch((error: unknown) => { - markDelegationFinished(record, 'failed', { - error: error instanceof Error ? error.message : '未知错误', - }) - }) - + const record = startDelegationContinuation(ctx, args.delegationId, args.message) const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), DEFAULT_WAIT_SECONDS * 1000)) await Promise.race([record.completion, timeout]) @@ -1062,6 +1078,18 @@ export async function injectAgentCollaborationMcpServer( }) }, ), + sdk.tool( + 'continue_delegation_async', + '异步向已完成、已失败、已取消或已中断的协作子会话追加后续指令。启动后立即返回,不等待子会话完成;请稍后使用 wait_for_delegations 或 get_delegation_results 获取结果。', + schemas.continueD, + async (args) => { + const record = startDelegationContinuation(ctx, args.delegationId, args.message) + return jsonResult({ + delegation: getDelegationSummary(record), + note: '后续指令已异步启动。请使用 wait_for_delegations 或 get_delegation_results 获取结果。', + }) + }, + ), ], }) @@ -1355,58 +1383,14 @@ export function buildPiCollaborationTools( sdk.defineTool({ name: 'mcp__collaboration__continue_delegation', label: '追加后续指令', - description: '向已完成、已失败、已取消或已中断的协作子会话追加后续指令。子会话保留完整上下文继续执行。', + description: '向已完成、已失败、已取消或已中断的协作子会话追加后续指令。子会话保留完整上下文继续执行。此工具会等待本轮完成;如需立即返回,请使用 continue_delegation_async。', parameters: Type.Object({ delegationId: Type.String({ description: '要继续操作的委派 ID' }), message: Type.String({ description: '追加给子 Agent 的后续指令' }), }), async execute(_toolCallId: string, params: unknown) { const args = params as { delegationId: string; message: string } - const record = getDelegationRecordForContinuation(ctx, args.delegationId) - if (!record) throw new Error(`未找到当前会话下的委派: ${args.delegationId}`) - if (record.status === 'running') { - throw new Error(`委派正在运行中,无法追加指令: ${args.delegationId}`) - } - - record.status = 'running' - record.error = undefined - record.resultSummary = undefined - record.completedAt = undefined - const completionHandle = createDelegationCompletion() - record.completion = completionHandle.completion - record.resolveCompletion = completionHandle.resolveCompletion - - updateAgentSessionMeta(record.childSessionId, { delegationStatus: 'running' }) - - runRegisteredHeadlessAgent( - { - sessionId: record.childSessionId, - userMessage: args.message, - channelId: record.channelId, - modelId: record.modelId, - workspaceId: ctx.workspaceId, - permissionModeOverride: record.permissionMode, - triggeredBy: 'delegation', - startedAt: Date.now(), - }, - { - source: 'delegation', - onError: (error) => { - markDelegationFinished(record, 'failed', { error }) - }, - onComplete: (messages) => { - if (record.status !== 'running') return - const resultSummary = summarizeChildResult(record.childSessionId, messages) - markDelegationFinished(record, 'completed', { resultSummary }) - }, - onTitleUpdated: () => {}, - }, - ).catch((error: unknown) => { - markDelegationFinished(record, 'failed', { - error: error instanceof Error ? error.message : '未知错误', - }) - }) - + const record = startDelegationContinuation(ctx, args.delegationId, args.message) const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), DEFAULT_WAIT_SECONDS * 1000)) await Promise.race([record.completion, timeout]) @@ -1416,5 +1400,24 @@ export function buildPiCollaborationTools( }) }, }), + sdk.defineTool({ + name: 'mcp__collaboration__continue_delegation_async', + label: '异步追加后续指令', + description: '异步向已完成、已失败、已取消或已中断的协作子会话追加后续指令。启动后立即返回,不等待子会话完成。', + parameters: Type.Object({ + delegationId: Type.String({ description: '要继续操作的委派 ID' }), + message: Type.String({ description: '追加给子 Agent 的后续指令' }), + }), + async execute(toolCallId: string, params: unknown) { + const args = params as { delegationId: string; message: string } + const delegationId = piContinueDelegationAsyncCalls.getOrCreate(ctx.sessionId, toolCallId, () => { + return startDelegationContinuation(ctx, args.delegationId, args.message).delegationId + }) + return piJsonResult({ + delegation: getDelegationResult(ctx.sessionId, delegationId), + note: '后续指令已异步启动。请使用 wait_for_delegations 或 get_delegation_results 获取结果。', + }) + }, + }), ] } diff --git a/apps/electron/src/main/lib/builtin-mcp/default-mcp.json b/apps/electron/src/main/lib/builtin-mcp/default-mcp.json index 6aaa1a803..71070681b 100644 --- a/apps/electron/src/main/lib/builtin-mcp/default-mcp.json +++ b/apps/electron/src/main/lib/builtin-mcp/default-mcp.json @@ -39,7 +39,10 @@ { "name": "list_delegations", "description": "列出当前父会话创建的子会话。", "readOnly": true }, { "name": "get_delegation_results", "description": "按委派 ID 读取子会话结果摘要。", "readOnly": true }, { "name": "stop_delegation", "description": "停止单个协作子会话。" }, - { "name": "stop_delegations", "description": "批量停止协作子会话。" } + { "name": "stop_delegations", "description": "批量停止协作子会话。" }, + { "name": "answer_delegation_question", "description": "代答子会话的阻塞问题或权限请求。" }, + { "name": "continue_delegation", "description": "追加后续指令并等待子会话完成。" }, + { "name": "continue_delegation_async", "description": "异步追加后续指令,不等待子会话完成。" } ] }, {