Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/electron/default-skills/agent-collaboration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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` 收敛结果。

## 先判断用哪种能力

Expand Down Expand Up @@ -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 写法

Expand Down
77 changes: 77 additions & 0 deletions apps/electron/src/main/lib/agent-collaboration-tools.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, Record<string, unknown>> = {}

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<T>(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')
})
})
187 changes: 95 additions & 92 deletions apps/electron/src/main/lib/agent-collaboration-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const delegations = new Map<string, DelegationRecord>()
// Pi 的 provider/retry 流可能重放同一个 tool call;委派会创建真实会话,必须幂等。
const piDelegateAgentCalls = createToolCallIdempotencyCache<PiDelegationToolResult>()
const piDelegateAgentsCalls = createToolCallIdempotencyCache<PiBatchDelegationResult>()
const piContinueDelegationAsyncCalls = createToolCallIdempotencyCache<string>()

// ===== 阻塞事件追踪(Level 1: Blocked Event Bubbling) =====

Expand Down Expand Up @@ -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'])
Expand Down Expand Up @@ -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])

Expand All @@ -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 获取结果。',
})
},
),
],
})

Expand Down Expand Up @@ -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])

Expand All @@ -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 获取结果。',
})
},
}),
]
}
5 changes: 4 additions & 1 deletion apps/electron/src/main/lib/builtin-mcp/default-mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "异步追加后续指令,不等待子会话完成。" }
]
},
{
Expand Down