hyacinth-ai 0.9.19 → 0.9.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/delegate-tool.d.ts +47 -1
- package/dist/agents/delegate-tool.js +122 -4
- package/dist/channels/dispatcher.d.ts +25 -0
- package/dist/channels/dispatcher.js +96 -0
- package/dist/channels/interface.d.ts +17 -0
- package/dist/channels/plugins/feishu/feishu-channel.d.ts +3 -1
- package/dist/channels/plugins/feishu/feishu-channel.js +104 -1
- package/dist/channels/plugins/feishu/feishu-send.d.ts +7 -0
- package/dist/channels/plugins/feishu/feishu-send.js +23 -0
- package/dist/gateway/factory.js +4 -0
- package/dist/gateway/tui.js +6 -0
- package/dist/lifecycle/supervisor.js +3 -0
- package/dist/orchestrator/loop.d.ts +8 -0
- package/dist/orchestrator/loop.js +19 -0
- package/dist/registry/tool.registry.js +4 -1
- package/dist/tools/runtime-control.d.ts +8 -0
- package/dist/tools/runtime-control.js +64 -0
- package/package.json +1 -1
|
@@ -25,6 +25,14 @@ export interface SubAgentContext {
|
|
|
25
25
|
sandboxRoot?: string;
|
|
26
26
|
/** 创建带独立 userId 的 Provider(子Agent 之间 + 与主Agent 的 KVCache 隔离) */
|
|
27
27
|
createSubProvider(userId: string): Provider;
|
|
28
|
+
/** 异步子Agent 结果推送队列(主 loop 的 pendingAsyncResults,完成后自动注入对话) */
|
|
29
|
+
pendingAsyncResults?: Array<{
|
|
30
|
+
handle: string;
|
|
31
|
+
agentName: string;
|
|
32
|
+
status: 'completed' | 'failed';
|
|
33
|
+
result?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
}>;
|
|
28
36
|
}
|
|
29
37
|
/** 注册正在运行的子 Agent loop */
|
|
30
38
|
export declare function registerRunningLoop(instanceId: string, loop: AgentLoop): void;
|
|
@@ -32,6 +40,29 @@ export declare function registerRunningLoop(instanceId: string, loop: AgentLoop)
|
|
|
32
40
|
export declare function unregisterRunningLoop(instanceId: string): void;
|
|
33
41
|
/** 中断指定子 Agent */
|
|
34
42
|
export declare function interruptSubAgentLoop(instanceId: string): boolean;
|
|
43
|
+
export interface AsyncSubAgentTask {
|
|
44
|
+
handle: string;
|
|
45
|
+
agentName: string;
|
|
46
|
+
instanceId: string;
|
|
47
|
+
task: string;
|
|
48
|
+
status: 'running' | 'completed' | 'failed';
|
|
49
|
+
startTime: string;
|
|
50
|
+
endTime?: string;
|
|
51
|
+
result?: string;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
/** 注册异步子 Agent 任务,返回 handle */
|
|
55
|
+
export declare function registerAsyncTask(agentName: string, instanceId: string, task: string): string;
|
|
56
|
+
/** 标记异步任务为已完成 */
|
|
57
|
+
export declare function completeAsyncTask(handle: string, result: string): void;
|
|
58
|
+
/** 标记异步任务为失败 */
|
|
59
|
+
export declare function failAsyncTask(handle: string, error: string): void;
|
|
60
|
+
/** 获取所有异步任务列表 */
|
|
61
|
+
export declare function listAsyncTasks(): AsyncSubAgentTask[];
|
|
62
|
+
/** 按 handle 获取异步任务 */
|
|
63
|
+
export declare function getAsyncTask(handle: string): AsyncSubAgentTask | undefined;
|
|
64
|
+
/** 等待所有运行中的异步子 Agent 任务完成(用于优雅关闭) */
|
|
65
|
+
export declare function waitForAsyncTasks(timeoutMs?: number): Promise<void>;
|
|
35
66
|
/** 销毁子 Agent session 目录(清理 conversation/events/stats/meta) */
|
|
36
67
|
export declare function destroySubAgentSession(parentSessionDir: string, instanceId: string): Promise<boolean>;
|
|
37
68
|
/**
|
|
@@ -73,6 +104,14 @@ export declare class DelegateToAgentTool implements Tool {
|
|
|
73
104
|
type: "string";
|
|
74
105
|
description: string;
|
|
75
106
|
};
|
|
107
|
+
async: {
|
|
108
|
+
type: "boolean";
|
|
109
|
+
description: string;
|
|
110
|
+
};
|
|
111
|
+
across_turns: {
|
|
112
|
+
type: "boolean";
|
|
113
|
+
description: string;
|
|
114
|
+
};
|
|
76
115
|
};
|
|
77
116
|
required: string[];
|
|
78
117
|
};
|
|
@@ -81,7 +120,14 @@ export declare class DelegateToAgentTool implements Tool {
|
|
|
81
120
|
constructor(agentRegistry: AgentRegistry, parentContext: SubAgentContext);
|
|
82
121
|
execute(args: Record<string, unknown>): Promise<string>;
|
|
83
122
|
/**
|
|
84
|
-
*
|
|
123
|
+
* 异步委托:在后台启动子 Agent,立即返回任务句柄。
|
|
124
|
+
* 子 Agent 的 loop.run() 在后台 Promise 中执行,完成后结果写入 AsyncSubAgentTask。
|
|
125
|
+
*/
|
|
126
|
+
private executeAsync;
|
|
127
|
+
/** 后台执行子 Agent 任务 */
|
|
128
|
+
private runAsyncInBackground;
|
|
129
|
+
/**
|
|
130
|
+
* 委托模式:单个子 Agent 执行任务(同步阻塞)
|
|
85
131
|
*/
|
|
86
132
|
private executeDelegate;
|
|
87
133
|
/**
|
|
@@ -33,6 +33,57 @@ export function interruptSubAgentLoop(instanceId) {
|
|
|
33
33
|
loop.interrupt();
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
|
+
const asyncTasks = new Map();
|
|
37
|
+
let asyncTaskCounter = 0;
|
|
38
|
+
/** 注册异步子 Agent 任务,返回 handle */
|
|
39
|
+
export function registerAsyncTask(agentName, instanceId, task) {
|
|
40
|
+
const handle = `sub_${String(++asyncTaskCounter).padStart(3, '0')}`;
|
|
41
|
+
asyncTasks.set(handle, {
|
|
42
|
+
handle,
|
|
43
|
+
agentName,
|
|
44
|
+
instanceId,
|
|
45
|
+
task,
|
|
46
|
+
status: 'running',
|
|
47
|
+
startTime: new Date().toISOString(),
|
|
48
|
+
});
|
|
49
|
+
return handle;
|
|
50
|
+
}
|
|
51
|
+
/** 标记异步任务为已完成 */
|
|
52
|
+
export function completeAsyncTask(handle, result) {
|
|
53
|
+
const t = asyncTasks.get(handle);
|
|
54
|
+
if (t) {
|
|
55
|
+
t.status = 'completed';
|
|
56
|
+
t.endTime = new Date().toISOString();
|
|
57
|
+
t.result = result;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** 标记异步任务为失败 */
|
|
61
|
+
export function failAsyncTask(handle, error) {
|
|
62
|
+
const t = asyncTasks.get(handle);
|
|
63
|
+
if (t) {
|
|
64
|
+
t.status = 'failed';
|
|
65
|
+
t.endTime = new Date().toISOString();
|
|
66
|
+
t.error = error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** 获取所有异步任务列表 */
|
|
70
|
+
export function listAsyncTasks() {
|
|
71
|
+
return [...asyncTasks.values()];
|
|
72
|
+
}
|
|
73
|
+
/** 按 handle 获取异步任务 */
|
|
74
|
+
export function getAsyncTask(handle) {
|
|
75
|
+
return asyncTasks.get(handle);
|
|
76
|
+
}
|
|
77
|
+
/** 等待所有运行中的异步子 Agent 任务完成(用于优雅关闭) */
|
|
78
|
+
export async function waitForAsyncTasks(timeoutMs = 5000) {
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
while (Date.now() - start < timeoutMs) {
|
|
81
|
+
const running = listAsyncTasks().filter(t => t.status === 'running');
|
|
82
|
+
if (running.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
36
87
|
/** 销毁子 Agent session 目录(清理 conversation/events/stats/meta) */
|
|
37
88
|
export async function destroySubAgentSession(parentSessionDir, instanceId) {
|
|
38
89
|
const subDir = path.join(parentSessionDir, 'sub-agents', instanceId);
|
|
@@ -57,6 +108,11 @@ export async function createSubAgentLoop(agentDef, task, parentContext) {
|
|
|
57
108
|
const instanceId = agentDef.instanceId ?? `${agentDef.name}-default`;
|
|
58
109
|
const subSessionDir = path.join(parentContext.sessionDir, 'sub-agents', instanceId);
|
|
59
110
|
const ttlMinutes = agentDef.sessionTtlMinutes ?? 10;
|
|
111
|
+
// 并发保护:同一 instanceId 已有活跃 loop 时拒绝,防止会话文件并发写入冲突
|
|
112
|
+
if (runningLoops.has(instanceId)) {
|
|
113
|
+
throw new Error(`子 Agent "${agentDef.name}" (${instanceId}) 正在执行中,不能同时委派第二个任务。` +
|
|
114
|
+
'等待当前任务完成后重试,或使用 spawn_sub_agent 创建独立实例来并行执行。');
|
|
115
|
+
}
|
|
60
116
|
// TTL 检查:过期则清理重建
|
|
61
117
|
let isNew = false;
|
|
62
118
|
try {
|
|
@@ -163,6 +219,7 @@ export class DelegateToAgentTool {
|
|
|
163
219
|
'"parallel"(并行分工):同时启动多个子 Agent 并行执行,汇总结果。' +
|
|
164
220
|
'编排场景:当你有一个复杂计划时,自己负责规划和决策,将其中可并行的子任务分别委派给多个子 Agent 同步执行——spawn_sub_agent 可克隆多份实例,配合不同的 instance_id 并发调度,大幅缩短总耗时。' +
|
|
165
221
|
'会话复用:子 Agent 会话在 TTL 窗口内持久化(默认 10 分钟),相同 instance_id 再次委派时自动恢复完整对话记忆,无需重新交代背景。' +
|
|
222
|
+
'异步执行:设置 async=true 后子 Agent 在后台运行,主 Agent 立即获得任务句柄(如 sub_001)并可继续其他工作。之后用 list_sub_agent_tasks 查看所有异步任务状态,get_sub_agent_result <handle> 获取已完成任务的结果。默认 async=false(同步阻塞,等待结果返回)。' +
|
|
166
223
|
'使用 list_sub_agents 查看可用 Agent 及其实例 ID,create_sub_agent 创建新 Agent,spawn_sub_agent 克隆以支持并行,destroy_sub_agent 清理不再需要的实例。';
|
|
167
224
|
inputSchema = {
|
|
168
225
|
type: 'object',
|
|
@@ -183,6 +240,14 @@ export class DelegateToAgentTool {
|
|
|
183
240
|
type: 'string',
|
|
184
241
|
description: '可选的附加上下文(如相关代码片段、文件路径、背景信息)。',
|
|
185
242
|
},
|
|
243
|
+
async: {
|
|
244
|
+
type: 'boolean',
|
|
245
|
+
description: '是否异步执行。默认 false(阻塞等待结果)。设为 true 时立即返回任务句柄(如 sub_001),子 Agent 在后台执行,主 Agent 可继续其他工具调用。',
|
|
246
|
+
},
|
|
247
|
+
across_turns: {
|
|
248
|
+
type: 'boolean',
|
|
249
|
+
description: '异步结果是否允许跨 turn。默认 false——子Agent完成后结果在当前 turn 内自动注入对话,LLM 立即看到并继续处理。设为 true 时结果不自动注入,需手动用 get_sub_agent_result 获取(适用于需要跨多轮对话的后台任务)。仅在 async=true 时生效。',
|
|
250
|
+
},
|
|
186
251
|
},
|
|
187
252
|
required: ['task'],
|
|
188
253
|
};
|
|
@@ -197,6 +262,8 @@ export class DelegateToAgentTool {
|
|
|
197
262
|
const agentName = args.agent_name;
|
|
198
263
|
const task = args.task;
|
|
199
264
|
const context = args.context;
|
|
265
|
+
const runAsync = args.async === true;
|
|
266
|
+
const acrossTurns = args.across_turns === true;
|
|
200
267
|
if (!agentName && !instanceId) {
|
|
201
268
|
return 'Error: either agent_name or instance_id is required. Use list_sub_agents to see available agents and their instance IDs.';
|
|
202
269
|
}
|
|
@@ -219,19 +286,23 @@ export class DelegateToAgentTool {
|
|
|
219
286
|
if (!agentDef) {
|
|
220
287
|
return 'Error: no sub-agent found matching the provided criteria.';
|
|
221
288
|
}
|
|
289
|
+
// 异步模式下不支持 adversarial 和 parallel(这两种本身就是多Agent协同,异步应逐个个委派)
|
|
290
|
+
if (runAsync && agentDef.collaborationMode !== 'delegate') {
|
|
291
|
+
return `错误:异步模式仅支持 collaborationMode="delegate"。当前模式为 "${agentDef.collaborationMode}"。请用 spawn_sub_agent 克隆独立实例,然后对每个实例分别用 async=true 委派。`;
|
|
292
|
+
}
|
|
222
293
|
// 构造完整任务描述
|
|
223
294
|
const fullTask = context ? `${task}\n\nAdditional Context:\n${context}` : task;
|
|
224
295
|
try {
|
|
296
|
+
if (runAsync) {
|
|
297
|
+
return await this.executeAsync(agentDef, fullTask, acrossTurns);
|
|
298
|
+
}
|
|
225
299
|
if (agentDef.collaborationMode === 'adversarial') {
|
|
226
|
-
// 对抗审查模式:找一个不同角色的子 Agent 同时审查
|
|
227
300
|
return await this.executeAdversarial(agentDef, fullTask);
|
|
228
301
|
}
|
|
229
302
|
else if (agentDef.collaborationMode === 'parallel') {
|
|
230
|
-
// 并行分工模式:所有 parallel 模式的子 Agent 并行执行
|
|
231
303
|
return await this.executeParallel(agentDef, fullTask);
|
|
232
304
|
}
|
|
233
305
|
else {
|
|
234
|
-
// 委托模式:直接委托给子 Agent
|
|
235
306
|
return await this.executeDelegate(agentDef, fullTask);
|
|
236
307
|
}
|
|
237
308
|
}
|
|
@@ -241,7 +312,54 @@ export class DelegateToAgentTool {
|
|
|
241
312
|
}
|
|
242
313
|
}
|
|
243
314
|
/**
|
|
244
|
-
*
|
|
315
|
+
* 异步委托:在后台启动子 Agent,立即返回任务句柄。
|
|
316
|
+
* 子 Agent 的 loop.run() 在后台 Promise 中执行,完成后结果写入 AsyncSubAgentTask。
|
|
317
|
+
*/
|
|
318
|
+
async executeAsync(agentDef, task, acrossTurns) {
|
|
319
|
+
const instanceId = agentDef.instanceId;
|
|
320
|
+
const handle = registerAsyncTask(agentDef.name, instanceId, task);
|
|
321
|
+
// 在后台启动子 Agent,不阻塞。catch 兜底防止未处理的 Promise rejection
|
|
322
|
+
this.runAsyncInBackground(handle, agentDef, task, acrossTurns).catch((err) => {
|
|
323
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
324
|
+
failAsyncTask(handle, message);
|
|
325
|
+
if (!acrossTurns) {
|
|
326
|
+
this.parentContext.pendingAsyncResults?.push({ handle, agentName: agentDef.name, status: 'failed', error: message });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
const hint = acrossTurns
|
|
330
|
+
? `跨 turn 模式——任务完成后需手动用 get_sub_agent_result ${handle} 获取结果。`
|
|
331
|
+
: '回合内模式——子Agent完成后结果将自动注入当前对话。';
|
|
332
|
+
return `异步子 Agent 任务已启动。\n句柄: ${handle}\nAgent: ${agentDef.name} (${instanceId})\n${hint}\n任务: ${task.slice(0, 100)}${task.length > 100 ? '...' : ''}`;
|
|
333
|
+
}
|
|
334
|
+
/** 后台执行子 Agent 任务 */
|
|
335
|
+
async runAsyncInBackground(handle, agentDef, task, acrossTurns) {
|
|
336
|
+
const instanceId = agentDef.instanceId;
|
|
337
|
+
try {
|
|
338
|
+
const { loop, sessionDir, isNew } = await createSubAgentLoop(agentDef, task, this.parentContext);
|
|
339
|
+
registerRunningLoop(instanceId, loop);
|
|
340
|
+
if (!isNew) {
|
|
341
|
+
await fs.utimes(path.join(sessionDir, 'meta.json'), new Date(), new Date()).catch(() => { });
|
|
342
|
+
}
|
|
343
|
+
await loop.run(task);
|
|
344
|
+
unregisterRunningLoop(instanceId);
|
|
345
|
+
const result = await this.collectResult(agentDef, task);
|
|
346
|
+
completeAsyncTask(handle, result);
|
|
347
|
+
if (!acrossTurns) {
|
|
348
|
+
// 回合内模式:推送到主 loop 队列,当前 turn 自动注入结果
|
|
349
|
+
this.parentContext.pendingAsyncResults?.push({ handle, agentName: agentDef.name, status: 'completed', result });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
unregisterRunningLoop(instanceId);
|
|
354
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
355
|
+
failAsyncTask(handle, message);
|
|
356
|
+
if (!acrossTurns) {
|
|
357
|
+
this.parentContext.pendingAsyncResults?.push({ handle, agentName: agentDef.name, status: 'failed', error: message });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 委托模式:单个子 Agent 执行任务(同步阻塞)
|
|
245
363
|
*/
|
|
246
364
|
async executeDelegate(agentDef, task) {
|
|
247
365
|
const instanceId = agentDef.instanceId;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Tool } from '../tools/interface.js';
|
|
2
|
+
import type { ChannelManager } from './manager.js';
|
|
3
|
+
/**
|
|
4
|
+
* MessageDispatcher — 统一分发层。
|
|
5
|
+
* 查目标渠道 → 调其 send() → 返回结果。
|
|
6
|
+
*/
|
|
7
|
+
export declare class MessageDispatcher {
|
|
8
|
+
private channelManager;
|
|
9
|
+
constructor(channelManager: ChannelManager);
|
|
10
|
+
send(args: {
|
|
11
|
+
channel: string;
|
|
12
|
+
to: string;
|
|
13
|
+
targetType?: 'user' | 'chat';
|
|
14
|
+
text?: string;
|
|
15
|
+
images?: Array<{
|
|
16
|
+
data: string;
|
|
17
|
+
media_type: string;
|
|
18
|
+
}>;
|
|
19
|
+
}): Promise<string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* send_channel_message 工具——暴露给 Agent 的跨渠道发送能力。
|
|
23
|
+
*/
|
|
24
|
+
export declare function createSendChannelMessageTool(dispatcher: MessageDispatcher): Tool;
|
|
25
|
+
//# sourceMappingURL=dispatcher.d.ts.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// MessageDispatcher — 跨渠道消息分发层
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// 允许 Agent 从任意会话借用其他渠道的发送能力。
|
|
6
|
+
// 消息是一次性的——不创建 session、不写 conversation、不影响任何渠道的对话状态。
|
|
7
|
+
// 被借用的渠道对"谁借了道"完全无感。
|
|
8
|
+
// ============================================================
|
|
9
|
+
/**
|
|
10
|
+
* MessageDispatcher — 统一分发层。
|
|
11
|
+
* 查目标渠道 → 调其 send() → 返回结果。
|
|
12
|
+
*/
|
|
13
|
+
export class MessageDispatcher {
|
|
14
|
+
channelManager;
|
|
15
|
+
constructor(channelManager) {
|
|
16
|
+
this.channelManager = channelManager;
|
|
17
|
+
}
|
|
18
|
+
async send(args) {
|
|
19
|
+
const state = this.channelManager.get(args.channel);
|
|
20
|
+
if (!state || state.status !== 'active') {
|
|
21
|
+
return `渠道 "${args.channel}" 未连接或未启动。可用渠道: ${this.channelManager.getStatusSummary().map(s => s.id).join(', ') || '无'}`;
|
|
22
|
+
}
|
|
23
|
+
const handler = state.handler;
|
|
24
|
+
if (!handler.send) {
|
|
25
|
+
return `渠道 "${args.channel}" (${handler.name}) 不支持主动发送。`;
|
|
26
|
+
}
|
|
27
|
+
const target = {
|
|
28
|
+
type: args.targetType ?? 'user',
|
|
29
|
+
id: args.to,
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const result = await handler.send(target, {
|
|
33
|
+
content: args.text ?? '',
|
|
34
|
+
images: args.images,
|
|
35
|
+
});
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return `发送失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* send_channel_message 工具——暴露给 Agent 的跨渠道发送能力。
|
|
45
|
+
*/
|
|
46
|
+
export function createSendChannelMessageTool(dispatcher) {
|
|
47
|
+
return {
|
|
48
|
+
name: 'send_channel_message',
|
|
49
|
+
description: '借用指定渠道发送消息到目标用户或群聊。纯借用渠道能力——不创建会话、不写入对话记录、不影响其他渠道。' +
|
|
50
|
+
'适用于从当前渠道(如 TUI)通过飞书/微信等渠道向用户发送通知、图片或文件。',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
channel: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: '目标渠道 ID。可用 channel_info 查看已连接渠道。如 "feishu"、"clawbot"。',
|
|
57
|
+
},
|
|
58
|
+
to: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: '目标用户 ID 或群聊 ID。飞书为 open_id 或 chat_id,微信为 wxid。',
|
|
61
|
+
},
|
|
62
|
+
target_type: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
enum: ['user', 'chat'],
|
|
65
|
+
description: '目标类型。user=私聊,chat=群聊。默认 user。',
|
|
66
|
+
},
|
|
67
|
+
text: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: '要发送的文本内容。',
|
|
70
|
+
},
|
|
71
|
+
images: {
|
|
72
|
+
type: 'array',
|
|
73
|
+
items: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
data: { type: 'string', description: '图片的 base64 数据' },
|
|
77
|
+
media_type: { type: 'string', description: 'MIME 类型,如 image/png' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
description: '要发送的图片列表(base64 + MIME 类型)。',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
required: ['channel', 'to'],
|
|
84
|
+
},
|
|
85
|
+
async execute(args) {
|
|
86
|
+
return dispatcher.send({
|
|
87
|
+
channel: args.channel,
|
|
88
|
+
to: args.to,
|
|
89
|
+
targetType: args.target_type ?? 'user',
|
|
90
|
+
text: args.text || undefined,
|
|
91
|
+
images: args.images,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=dispatcher.js.map
|
|
@@ -45,6 +45,13 @@ export interface ChannelReply {
|
|
|
45
45
|
}>;
|
|
46
46
|
metadata?: Record<string, unknown>;
|
|
47
47
|
}
|
|
48
|
+
/** 主动发送的目标(跨渠道借用能力时指定接收方) */
|
|
49
|
+
export interface ChannelTarget {
|
|
50
|
+
/** 目标类型 */
|
|
51
|
+
type: 'user' | 'chat';
|
|
52
|
+
/** 用户 ID 或群聊 ID */
|
|
53
|
+
id: string;
|
|
54
|
+
}
|
|
48
55
|
/** 输出处理器(渠道消息收集用) */
|
|
49
56
|
export interface ChannelOutputHandler {
|
|
50
57
|
onText?(content: string): void;
|
|
@@ -137,6 +144,16 @@ export interface ChannelHandler {
|
|
|
137
144
|
* 获取渠道状态
|
|
138
145
|
*/
|
|
139
146
|
getStatus(): ChannelStatus;
|
|
147
|
+
/**
|
|
148
|
+
* 主动发送消息到渠道(跨 session,不关联任何 AgentLoop、不落盘)。
|
|
149
|
+
* 用于从其他渠道借用本渠道的发送能力(如 TUI 中的 Agent 通过飞书发消息)。
|
|
150
|
+
* 未实现 = 该渠道不支持被外部借用。
|
|
151
|
+
*
|
|
152
|
+
* @param target 目标用户或群聊
|
|
153
|
+
* @param content 消息内容
|
|
154
|
+
* @returns 发送结果描述
|
|
155
|
+
*/
|
|
156
|
+
send?(target: ChannelTarget, content: ChannelReply): Promise<string>;
|
|
140
157
|
}
|
|
141
158
|
export type ChannelStatus = 'registered' | 'starting' | 'active' | 'stopped' | 'error';
|
|
142
159
|
export interface ChannelState {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelHandler, ChannelEvent, ChannelReply, ChannelConfig, ChannelStatus, ChannelMessageEvent, AgentFactory, ReplyFn } from '../../interface.js';
|
|
1
|
+
import type { ChannelHandler, ChannelEvent, ChannelReply, ChannelTarget, ChannelConfig, ChannelStatus, ChannelMessageEvent, AgentFactory, ReplyFn } from '../../interface.js';
|
|
2
2
|
export declare class FeishuChannel implements ChannelHandler {
|
|
3
3
|
readonly id = "feishu";
|
|
4
4
|
readonly name = "Feishu (\u98DE\u4E66)";
|
|
@@ -32,6 +32,8 @@ export declare class FeishuChannel implements ChannelHandler {
|
|
|
32
32
|
onEvent(handler: (event: ChannelEvent) => Promise<void>): void;
|
|
33
33
|
reply(sessionId: string, reply: ChannelReply): Promise<void>;
|
|
34
34
|
getStatus(): ChannelStatus;
|
|
35
|
+
send(target: ChannelTarget, content: ChannelReply): Promise<string>;
|
|
36
|
+
private uploadImage;
|
|
35
37
|
/** 获取 tenant access token(缓存,提前 60s 刷新,token 有效期 2h) */
|
|
36
38
|
private getTenantAccessToken;
|
|
37
39
|
/**
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { resolveFeishuConfig, validateFeishuConfig, } from './feishu-config.js';
|
|
15
15
|
import { FeishuTransport } from './feishu-transport.js';
|
|
16
16
|
import { parseFeishuMessageEvent, FeishuDedupeStore, checkDmAccess, checkGroupAccess, } from './feishu-event.js';
|
|
17
|
-
import { sendText, sendCard, getSenderInfo } from './feishu-send.js';
|
|
17
|
+
import { sendText, sendCard, sendImage, getSenderInfo } from './feishu-send.js';
|
|
18
18
|
import { ChannelSessionPool, createCollectHandler } from './feishu-session.js';
|
|
19
19
|
import { FeishuMessageQueue } from './feishu-message-queue.js';
|
|
20
20
|
import { generateSessionId } from '../../../memory/session.js';
|
|
@@ -176,6 +176,109 @@ export class FeishuChannel {
|
|
|
176
176
|
getStatus() {
|
|
177
177
|
return this.status;
|
|
178
178
|
}
|
|
179
|
+
// ================================================================
|
|
180
|
+
// send() — 跨渠道借用能力入口
|
|
181
|
+
// ================================================================
|
|
182
|
+
//
|
|
183
|
+
// 设计意图:
|
|
184
|
+
// 每个渠道有自己独特的发送能力(飞书能发卡片和图片、微信能发文件、
|
|
185
|
+
// TUI 只能显示文本)。send() 将这些能力暴露给外部,使得其他渠道
|
|
186
|
+
// 的 Agent 可以借用本渠道的能力发送消息。
|
|
187
|
+
//
|
|
188
|
+
// 行为保证("纯借用"语义):
|
|
189
|
+
// - 不创建 session —— 消息是一次性的,没有对应的 AgentLoop
|
|
190
|
+
// - 不写 conversation.jsonl —— 不污染任何渠道的对话历史
|
|
191
|
+
// - 不影响 ChannelHandler 内部状态 —— sessionMap、消息队列等完全不变
|
|
192
|
+
// - 被借用方对"谁借的"完全无感 —— 不需要知道调用方是哪个渠道
|
|
193
|
+
//
|
|
194
|
+
// 调用链:
|
|
195
|
+
// Agent → send_channel_message 工具 → MessageDispatcher
|
|
196
|
+
// → ChannelManager.get('feishu') → FeishuChannel.send()
|
|
197
|
+
//
|
|
198
|
+
// 支持的消息类型(按优先级):
|
|
199
|
+
// 1. 卡片消息(content.metadata.card 存在时)—— 飞书交互式卡片
|
|
200
|
+
// 2. 文本消息(默认)—— Post 格式,支持 Markdown 子集
|
|
201
|
+
// 3. 图片消息(content.images 存在时)—— 先上传获取 image_key,再发送
|
|
202
|
+
// ↑ 图片在文本/卡片之后独立发送,失败不影响前面的消息
|
|
203
|
+
//
|
|
204
|
+
// 注意:
|
|
205
|
+
// - 图片上传需要 tenant_access_token(通过 getTenantAccessToken 获取)
|
|
206
|
+
// - 每条图片独立上传 + 发送,某张失败不阻塞其他图片
|
|
207
|
+
// - 图片 base64 直接传给飞书 API,不在本地落盘
|
|
208
|
+
// ================================================================
|
|
209
|
+
async send(target, content) {
|
|
210
|
+
if (this.status !== 'active')
|
|
211
|
+
return '飞书渠道未连接,无法发送';
|
|
212
|
+
const config = this.config;
|
|
213
|
+
// 飞书 SDK 的 receive_id_type 由 ID 前缀推断(ou_→open_id, oc_→chat_id)
|
|
214
|
+
// sendText/sendCard/sendImage 内部调用 resolveSendTarget 自动处理
|
|
215
|
+
const to = target.type === 'chat' ? `chat:${target.id}` : `user:${target.id}`;
|
|
216
|
+
try {
|
|
217
|
+
// ① 主消息:卡片优先,否则文本
|
|
218
|
+
if (content.metadata?.card) {
|
|
219
|
+
await sendCard(config, { to, card: content.metadata.card });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
await sendText(config, { to, text: content.content });
|
|
223
|
+
}
|
|
224
|
+
// ② 图片:在文本/卡片之后独立发送
|
|
225
|
+
// 飞书的图片消息和文本消息是两条独立的消息,不支持图文混排
|
|
226
|
+
if (content.images && content.images.length > 0) {
|
|
227
|
+
const token = await this.getTenantAccessToken();
|
|
228
|
+
if (token) {
|
|
229
|
+
for (const img of content.images) {
|
|
230
|
+
try {
|
|
231
|
+
// 飞书图片发送两步:上传拿 image_key → 调用 im.message.create 发送
|
|
232
|
+
const imageKey = await this.uploadImage(token, img.data);
|
|
233
|
+
await sendImage(config, { to, imageKey });
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// 某张图片失败不阻塞其他图片和整体流程
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return `已通过飞书发送到 ${to}`;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
return `飞书发送失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ================================================================
|
|
248
|
+
// uploadImage() — 飞书图片上传
|
|
249
|
+
// ================================================================
|
|
250
|
+
//
|
|
251
|
+
// 飞书发送图片必须先上传到飞书服务器获取 image_key,
|
|
252
|
+
// 再用 image_key 调用 im.message.create(msg_type='image')发送。
|
|
253
|
+
//
|
|
254
|
+
// API: POST https://open.feishu.cn/open-apis/im/v1/images
|
|
255
|
+
// 参数: image_type='message'(消息用图,非头像),image=base64(不含 data:xxx;base64, 前缀)
|
|
256
|
+
// 返回: { code: 0, data: { image_key: 'img_xxx' } }
|
|
257
|
+
//
|
|
258
|
+
// 注意:
|
|
259
|
+
// - token 由调用方传入(来自 getTenantAccessToken 的缓存结果)
|
|
260
|
+
// - mediaType 参数保留供未来扩展(如格式校验),当前仅透传 base64
|
|
261
|
+
// - 飞书 image_key 有时效性(约 2 小时),不持久化缓存
|
|
262
|
+
// ================================================================
|
|
263
|
+
async uploadImage(token, base64Data, _mediaType) {
|
|
264
|
+
const domain = this.config?.domain ?? 'https://open.feishu.cn';
|
|
265
|
+
const resp = await fetch(`${domain}/open-apis/im/v1/images`, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
'Authorization': `Bearer ${token}`,
|
|
269
|
+
'Content-Type': 'application/json',
|
|
270
|
+
},
|
|
271
|
+
body: JSON.stringify({
|
|
272
|
+
image_type: 'message',
|
|
273
|
+
image: base64Data,
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
const json = await resp.json();
|
|
277
|
+
if (json.code !== 0 || !json.data?.image_key) {
|
|
278
|
+
throw new Error(`飞书图片上传失败: code=${json.code}`);
|
|
279
|
+
}
|
|
280
|
+
return json.data.image_key;
|
|
281
|
+
}
|
|
179
282
|
/** 获取 tenant access token(缓存,提前 60s 刷新,token 有效期 2h) */
|
|
180
283
|
async getTenantAccessToken() {
|
|
181
284
|
if (this.cachedToken && Date.now() < this.tokenExpiresAt - 60_000) {
|
|
@@ -41,4 +41,11 @@ export declare function getSenderInfo(config: FeishuChannelConfig, userId: strin
|
|
|
41
41
|
name?: string;
|
|
42
42
|
avatar?: string;
|
|
43
43
|
} | null>;
|
|
44
|
+
/**
|
|
45
|
+
* 发送图片消息(需先通过上传 API 获取 image_key)。
|
|
46
|
+
*/
|
|
47
|
+
export declare function sendImage(config: FeishuChannelConfig, params: {
|
|
48
|
+
to: string;
|
|
49
|
+
imageKey: string;
|
|
50
|
+
}): Promise<FeishuSendResult>;
|
|
44
51
|
//# sourceMappingURL=feishu-send.d.ts.map
|
|
@@ -229,4 +229,27 @@ export async function getSenderInfo(config, userId, userIdType = 'open_id') {
|
|
|
229
229
|
return null;
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* 发送图片消息(需先通过上传 API 获取 image_key)。
|
|
234
|
+
*/
|
|
235
|
+
export async function sendImage(config, params) {
|
|
236
|
+
const client = await createFeishuClient(config);
|
|
237
|
+
const { receiveId, receiveIdType } = resolveSendTarget(params.to);
|
|
238
|
+
const content = JSON.stringify({ image_key: params.imageKey });
|
|
239
|
+
const res = await client.im.message.create({
|
|
240
|
+
params: { receive_id_type: receiveIdType },
|
|
241
|
+
data: {
|
|
242
|
+
receive_id: receiveId,
|
|
243
|
+
content,
|
|
244
|
+
msg_type: 'image',
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
if (res.code !== 0) {
|
|
248
|
+
throw new Error(`飞书图片发送失败: ${res.msg || `code ${res.code}`}`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
messageId: res.data?.message_id ?? '',
|
|
252
|
+
chatId: receiveId,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
232
255
|
//# sourceMappingURL=feishu-send.js.map
|
package/dist/gateway/factory.js
CHANGED
|
@@ -424,12 +424,15 @@ export async function createAgent(options, supervisor) {
|
|
|
424
424
|
for (const agent of agents) {
|
|
425
425
|
agentRegistry.register(agent);
|
|
426
426
|
}
|
|
427
|
+
// 异步子Agent 结果队列——先创建引用,后续赋给 loop
|
|
428
|
+
const pendingAsyncResults = [];
|
|
427
429
|
const delegateTool = new DelegateToAgentTool(agentRegistry, {
|
|
428
430
|
modelRouter,
|
|
429
431
|
toolRegistry,
|
|
430
432
|
sessionDir,
|
|
431
433
|
maxContextTokens: maxContext,
|
|
432
434
|
dependencyAnalyzer,
|
|
435
|
+
pendingAsyncResults,
|
|
433
436
|
createSubProvider: (userId) => {
|
|
434
437
|
if (!subProviderApiKey || !subProviderBaseType) {
|
|
435
438
|
throw new Error('Cannot create sub-agent provider: main provider not configured.');
|
|
@@ -517,6 +520,7 @@ export async function createAgent(options, supervisor) {
|
|
|
517
520
|
const loop = new AgentLoop(provider, contextComposer, compressor, orchestrator, toolExecutor, toolRegistry, conversationStore, eventStore, statsManager, sessionDir, summaryStore, effectiveMaxTurns, maxContext, outputHandler, skillRegistry, mcpSystem.getBridge(), dependencyAnalyzer, agentRegistry, effectivePersonaDir, flowRegistry, providerRouter, new Set(config.safety?.dangerousTools ?? ['write', 'bash']), new Set(), configCenter, modelRouter, turnRecorder);
|
|
518
521
|
// 注入知识库状态引用
|
|
519
522
|
loop.kbState = kbState;
|
|
523
|
+
loop.pendingAsyncResults = pendingAsyncResults; // 异步子Agent结果队列(和 delegateTool 共享引用)
|
|
520
524
|
loopRef = loop; // wire fallback notification
|
|
521
525
|
// 注入旁路 Provider 引用(供模式切换时 setBypassUserId 使用)
|
|
522
526
|
if (bypassProvider)
|
package/dist/gateway/tui.js
CHANGED
|
@@ -970,6 +970,12 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
970
970
|
}
|
|
971
971
|
// 启动所有渠道(每个渠道自行处理消息)
|
|
972
972
|
await channelManager.startAll(agentFactory);
|
|
973
|
+
// 注册跨渠道发送工具(TUI 模式)
|
|
974
|
+
if (agent) {
|
|
975
|
+
const { MessageDispatcher, createSendChannelMessageTool } = await import('../channels/dispatcher.js');
|
|
976
|
+
const dispatcher = new MessageDispatcher(channelManager);
|
|
977
|
+
agent.toolRegistry.register(createSendChannelMessageTool(dispatcher));
|
|
978
|
+
}
|
|
973
979
|
// ── ASCII Art loader ──────────────────────────────────────────────
|
|
974
980
|
async function loadAsciiArt(maxWidth = 54) {
|
|
975
981
|
const asciiDir = path.join(os.homedir(), '.agent', 'ascii');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ProcessManager } from './manager.js';
|
|
2
2
|
import { LocalModelManager } from './local-model.js';
|
|
3
|
+
import { waitForAsyncTasks } from '../agents/delegate-tool.js';
|
|
3
4
|
/**
|
|
4
5
|
* LifecycleSupervisor — 统一管理所有受管进程的生命周期。
|
|
5
6
|
*
|
|
@@ -184,6 +185,8 @@ export class LifecycleSupervisor {
|
|
|
184
185
|
if (this.backgroundRegistry) {
|
|
185
186
|
await this.backgroundRegistry.shutdownAll().catch(() => { });
|
|
186
187
|
}
|
|
188
|
+
// 等待异步子 Agent 任务完成(最多等 5 秒)
|
|
189
|
+
await waitForAsyncTasks(5000).catch(() => { });
|
|
187
190
|
const results = this.entities.map((e) => e.manager.stop().catch(() => {
|
|
188
191
|
// 单个关闭失败不影响其他
|
|
189
192
|
}));
|
|
@@ -151,6 +151,14 @@ export declare class AgentLoop {
|
|
|
151
151
|
name: string;
|
|
152
152
|
firedAt: string;
|
|
153
153
|
}>;
|
|
154
|
+
/** 异步子Agent 完成后的结果队列(delegate-tool 写入,主循环消费) */
|
|
155
|
+
pendingAsyncResults: Array<{
|
|
156
|
+
handle: string;
|
|
157
|
+
agentName: string;
|
|
158
|
+
status: 'completed' | 'failed';
|
|
159
|
+
result?: string;
|
|
160
|
+
error?: string;
|
|
161
|
+
}>;
|
|
154
162
|
/** 当前正在执行的定时任务名(供 TUI 显示上下文),run 前设置,run 后清除 */
|
|
155
163
|
pendingTaskName: string | null;
|
|
156
164
|
/** 知识库状态引用(factory 注入) */
|
|
@@ -203,6 +203,8 @@ export class AgentLoop {
|
|
|
203
203
|
schedulerInitialized = false;
|
|
204
204
|
/** 定时任务触发后待注入对话的通知 */
|
|
205
205
|
pendingTaskNotifications = [];
|
|
206
|
+
/** 异步子Agent 完成后的结果队列(delegate-tool 写入,主循环消费) */
|
|
207
|
+
pendingAsyncResults = [];
|
|
206
208
|
/** 当前正在执行的定时任务名(供 TUI 显示上下文),run 前设置,run 后清除 */
|
|
207
209
|
pendingTaskName = null;
|
|
208
210
|
/** 知识库状态引用(factory 注入) */
|
|
@@ -1159,6 +1161,23 @@ export class AgentLoop {
|
|
|
1159
1161
|
turnCount++;
|
|
1160
1162
|
if (result.toolCalled)
|
|
1161
1163
|
toolWasCalled = true;
|
|
1164
|
+
// ── 异步子Agent 结果回合内注入 ─────────────────────────
|
|
1165
|
+
// delegate-tool 中异步任务完成后会将结果推送到此队列。
|
|
1166
|
+
// 本轮迭代结束后检查:有已完成的结果→注入对话→强制继续迭代,
|
|
1167
|
+
// 让 LLM 在当前 turn 内拿到结果并做出反应,无需跨 turn 手动查。
|
|
1168
|
+
if (this.pendingAsyncResults.length > 0) {
|
|
1169
|
+
const results = this.pendingAsyncResults.splice(0);
|
|
1170
|
+
for (const r of results) {
|
|
1171
|
+
const content = r.status === 'completed'
|
|
1172
|
+
? `[异步子Agent ${r.handle} (${r.agentName}) 已完成]\n${r.result ?? ''}`
|
|
1173
|
+
: `[异步子Agent ${r.handle} (${r.agentName}) 执行失败]\n${r.error ?? ''}`;
|
|
1174
|
+
await this.conversationStore.append(this.sessionDir, {
|
|
1175
|
+
role: 'user',
|
|
1176
|
+
content: [{ type: 'text', text: content }],
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
result.stop = false; // 强制继续迭代,让 LLM 看到注入的异步结果
|
|
1180
|
+
}
|
|
1162
1181
|
// 更新 stats
|
|
1163
1182
|
await this.statsManager.increment(this.sessionDir, 'turn_count', 1);
|
|
1164
1183
|
// ── 旁路Agent 迭代审查(每次 LLM 回复后) ──────────
|
|
@@ -3,7 +3,7 @@ import { getActiveRouterName } from '../context/profiles.js';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import { createGetConfigTool, createUpdateConfigTool, createConfigSchemaTool, createResetConfigTool, } from '../tools/config.js';
|
|
6
|
-
import { createSwitchProviderTool, createListProvidersTool, createProviderInfoTool, createSwitchToAutoRouteTool, createToggleToolTool, createListToolsTool, createToggleSkillTool, createListSkillsTool, createToggleSubAgentTool, createListSubAgentsTool, createSpawnSubAgentTool, createCreateSubAgentTool, createUpdateSubAgentTool, createInterruptTool, createSessionStatsTool, createCurrentSessionTool, createListSessionsTool, createNewSessionTool, createSwitchSessionTool, createDeleteSessionTool, createAllowToolTool, createDisallowToolTool, createListAllowlistTool, createAddTaskTool, createRemoveTaskTool, createListTasksTool, createToggleTaskTool, createMcpStatusTool, createListModelChannelsTool, createAddModelChannelTool, createRemoveModelChannelTool, createSetChannelRoleTool, createSetChannelModelTool, createResetChannelModelTool, createChannelInfoTool, } from '../tools/runtime-control.js';
|
|
6
|
+
import { createSwitchProviderTool, createListProvidersTool, createProviderInfoTool, createSwitchToAutoRouteTool, createToggleToolTool, createListToolsTool, createToggleSkillTool, createListSkillsTool, createToggleSubAgentTool, createListSubAgentsTool, createSpawnSubAgentTool, createCreateSubAgentTool, createUpdateSubAgentTool, createListSubAgentTasksTool, createGetSubAgentResultTool, createInterruptTool, createSessionStatsTool, createCurrentSessionTool, createListSessionsTool, createNewSessionTool, createSwitchSessionTool, createDeleteSessionTool, createAllowToolTool, createDisallowToolTool, createListAllowlistTool, createAddTaskTool, createRemoveTaskTool, createListTasksTool, createToggleTaskTool, createMcpStatusTool, createListModelChannelsTool, createAddModelChannelTool, createRemoveModelChannelTool, createSetChannelRoleTool, createSetChannelModelTool, createResetChannelModelTool, createChannelInfoTool, } from '../tools/runtime-control.js';
|
|
7
7
|
/**
|
|
8
8
|
* 工具注册表
|
|
9
9
|
* 负责注册、查询工具,以及生成 LLM 格式的工具定义
|
|
@@ -110,6 +110,9 @@ export class ToolRegistry extends GenericRegistry {
|
|
|
110
110
|
this.register(createSpawnSubAgentTool(agentRegistry));
|
|
111
111
|
this.register(createCreateSubAgentTool(agentRegistry, cwd));
|
|
112
112
|
this.register(createUpdateSubAgentTool(agentRegistry));
|
|
113
|
+
// ── 异步子 Agent 任务工具 (2) ─────────────────────────────────
|
|
114
|
+
this.register(createListSubAgentTasksTool());
|
|
115
|
+
this.register(createGetSubAgentResultTool());
|
|
113
116
|
// destroy_sub_agent needs factory.ts sessionDir; register in factory.ts after loop is created
|
|
114
117
|
// (handled by importing and registering createDestroySubAgentTool directly in factory.ts)
|
|
115
118
|
// ── Session control tools (7) ────────────────────────
|
|
@@ -51,6 +51,14 @@ export declare function createToggleSubAgentTool(agentRegistry: AgentRegistry, c
|
|
|
51
51
|
* list_sub_agents — list all registered sub-agents with their enabled/disabled status and instance IDs.
|
|
52
52
|
*/
|
|
53
53
|
export declare function createListSubAgentsTool(agentRegistry: any): Tool;
|
|
54
|
+
/**
|
|
55
|
+
* list_sub_agent_tasks — 列出所有异步子 Agent 任务及其状态。
|
|
56
|
+
*/
|
|
57
|
+
export declare function createListSubAgentTasksTool(): Tool;
|
|
58
|
+
/**
|
|
59
|
+
* get_sub_agent_result — 获取异步子 Agent 任务的执行结果。
|
|
60
|
+
*/
|
|
61
|
+
export declare function createGetSubAgentResultTool(): Tool;
|
|
54
62
|
/**
|
|
55
63
|
* spawn_sub_agent — clone an existing sub-agent to create a parallel instance (分身).
|
|
56
64
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clearPromptCache } from '../prompts/loader.js';
|
|
2
2
|
import { switchRouter } from '../context/profiles.js';
|
|
3
|
+
import { listAsyncTasks, getAsyncTask } from '../agents/delegate-tool.js';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import os from 'node:os';
|
|
@@ -359,6 +360,69 @@ export function createListSubAgentsTool(agentRegistry) {
|
|
|
359
360
|
},
|
|
360
361
|
};
|
|
361
362
|
}
|
|
363
|
+
// ── 异步子 Agent 任务管理工具 ──
|
|
364
|
+
/**
|
|
365
|
+
* list_sub_agent_tasks — 列出所有异步子 Agent 任务及其状态。
|
|
366
|
+
*/
|
|
367
|
+
export function createListSubAgentTasksTool() {
|
|
368
|
+
return {
|
|
369
|
+
name: 'list_sub_agent_tasks',
|
|
370
|
+
description: '列出所有通过 delegate_to_agent(async=true) 启动的异步子 Agent 任务。包含句柄、Agent 名称、任务描述、状态(running/completed/failed)、启动时间和结果摘要。',
|
|
371
|
+
inputSchema: { type: 'object', properties: {} },
|
|
372
|
+
async execute(_args) {
|
|
373
|
+
const tasks = listAsyncTasks();
|
|
374
|
+
if (tasks.length === 0) {
|
|
375
|
+
return '暂无异步子 Agent 任务。使用 delegate_to_agent 并设置 async=true 来启动异步任务。';
|
|
376
|
+
}
|
|
377
|
+
const lines = [`异步子 Agent 任务(共 ${tasks.length} 个):`];
|
|
378
|
+
for (const t of tasks) {
|
|
379
|
+
const statusLabel = t.status === 'running' ? '执行中' : t.status === 'completed' ? '已完成' : '失败';
|
|
380
|
+
const statusIcon = t.status === 'running' ? '🔄' : t.status === 'completed' ? '✅' : '❌';
|
|
381
|
+
lines.push(` ${statusIcon} ${t.handle} [${statusLabel}] ${t.agentName} (instance: ${t.instanceId}): ${t.task.slice(0, 60)}${t.task.length > 60 ? '...' : ''}`);
|
|
382
|
+
if (t.status !== 'running' && t.result) {
|
|
383
|
+
const summary = t.result.split('\n').find(l => l.trim())?.slice(0, 80) ?? '';
|
|
384
|
+
lines.push(` ↳ ${summary}${summary.length >= 80 ? '...' : ''}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
lines.push('');
|
|
388
|
+
lines.push('使用 get_sub_agent_result <handle> 获取已完成任务的完整结果。');
|
|
389
|
+
return lines.join('\n');
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* get_sub_agent_result — 获取异步子 Agent 任务的执行结果。
|
|
395
|
+
*/
|
|
396
|
+
export function createGetSubAgentResultTool() {
|
|
397
|
+
return {
|
|
398
|
+
name: 'get_sub_agent_result',
|
|
399
|
+
description: '获取指定异步子 Agent 任务的执行结果。running 时返回仍在执行中,completed 时返回完整结果,failed 时返回错误信息。句柄从 list_sub_agent_tasks 获取。',
|
|
400
|
+
inputSchema: {
|
|
401
|
+
type: 'object',
|
|
402
|
+
properties: {
|
|
403
|
+
handle: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: '异步任务句柄(如 sub_001),从 list_sub_agent_tasks 获取。',
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
required: ['handle'],
|
|
409
|
+
},
|
|
410
|
+
async execute(args) {
|
|
411
|
+
const handle = args.handle;
|
|
412
|
+
const task = getAsyncTask(handle);
|
|
413
|
+
if (!task) {
|
|
414
|
+
return `错误:未找到异步任务 "${handle}"。使用 list_sub_agent_tasks 查看所有任务及其句柄。`;
|
|
415
|
+
}
|
|
416
|
+
if (task.status === 'running') {
|
|
417
|
+
return `任务 ${handle}(${task.agentName})仍在执行中。任务:${task.task.slice(0, 100)}${task.task.length > 100 ? '...' : ''}\n启动时间:${task.startTime}\n请稍后再次调用 get_sub_agent_result 查询。`;
|
|
418
|
+
}
|
|
419
|
+
if (task.status === 'failed') {
|
|
420
|
+
return `任务 ${handle}(${task.agentName})执行失败。\n错误:${task.error}\n任务:${task.task}`;
|
|
421
|
+
}
|
|
422
|
+
return `任务 ${handle}(${task.agentName})已完成。\n\n${task.result}`;
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
362
426
|
/**
|
|
363
427
|
* spawn_sub_agent — clone an existing sub-agent to create a parallel instance (分身).
|
|
364
428
|
*/
|