deep-research.js
5.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* deep-research —— 按需触发 org-hunt 做一次轻量调研(46 号文档 Phase 2)。
*
* org-hunt 目前只能作为图节点被 `workgraph-planning` 动态选中调用(`deliverable.json`
* 模板,FDE 训练场景),交付物是 `knowhow_raw.md`/`crawl_config.json`/`seed_data.json`
* (≥50 条)那套重契约。这里**不**套用那套契约——"task 入队前的快速调研参考"是时间敏感
* 场景,直接照训练场景的产出量级要求会不可控地慢。org-hunt 的图节点契约、`deliverable.json`
* 模板本身都不动,这里只是另开一条**图外**的调用路径,让它以"轻量问答"的形式被叫起来
* (调用方式仿 `dream-service.callDreamAgent`:直接 `AgentManager.callAgent`,不经图引擎)。
*
* **默认关闭**(`ENABLE_DEEP_RESEARCH` 环境变量):org-hunt 的延迟/成功率在"日常任务规划、
* 时间敏感"场景下从未验证过(46 号文档「已知的高风险点」原话),且 subtask-planning 这条
* 触发链已经因为疏于隔离出过一次真事故(44 个残留 CLI 子进程,见 subtask-planning.js 头注释)。
* 不开这个开关,这个模块连 org-hunt 都不会尝试雇佣/调用——只打一行日志说明跳过。
*/
const path = require('path');
const RESEARCH_AGENT_ID = 'org-hunt';
// ── 注入点(仿 subtask-planning.setPlanSubtasks)──────────────────────
let triggerImpl = defaultTriggerDeepResearch;
let isInjected = false;
/** 替换调研实现(传 null 回默认实现)。测试用,避免真起 CLI */
function setTriggerDeepResearch(fn) {
triggerImpl = fn || defaultTriggerDeepResearch;
isInjected = !!fn;
}
/**
* @param {{companyId: string, task: object, topic: string}} ctx
* @returns {Promise<{findings: string, sources: string[], tokens: number} | null>}
* `null` = 没跑(开关关闭 / 测试环境未注入 / 调用失败 / 没解析出结构化结果)——
* 调用方一律当"没有调研结果"处理,不是错误,不该抛出去打断规划流程。
*/
async function triggerDeepResearch(ctx) {
if (process.env.ENABLE_DEEP_RESEARCH !== '1') {
console.log('[DeepResearch] ENABLE_DEEP_RESEARCH 未开启,跳过(默认关闭,见文件头注释)');
return null;
}
if (process.env.VITEST && !isInjected) {
console.log('[DeepResearch] VITEST 环境且未显式注入,跳过真实调用(避免测试意外起 CLI 子进程)');
return null;
}
try {
return await triggerImpl(ctx);
} catch (err) {
console.warn(`[DeepResearch] 触发失败,按"没有调研结果"处理:${err.message}`);
return null;
}
}
/** 寻址名。与 `subtask-planning.resolvePlanningAgentName` 同一个模式:按需幂等雇一次 */
function resolveResearchAgentName(companyId) {
if (!companyId) return RESEARCH_AGENT_ID;
const { CompanyService, listCompanyAgents } = require('./company-service');
const isHired = () => listCompanyAgents(companyId)
.some((r) => r.role_name === RESEARCH_AGENT_ID || r.system_agent_id === RESEARCH_AGENT_ID);
if (!isHired()) {
try {
new CompanyService().hire(companyId, RESEARCH_AGENT_ID, {});
} catch (e) {
console.warn(`[DeepResearch] 按需雇佣 ${RESEARCH_AGENT_ID} 失败(${companyId}): ${e.message}`);
}
}
return isHired() ? `${companyId}:${RESEARCH_AGENT_ID}` : RESEARCH_AGENT_ID;
}
function buildResearchPrompt({ topic }) {
return [
'这是一次**任务规划前的快速调研参考**,不是你平时的训练场景任务——',
'不需要按你 `agent.md` 平时的产出契约交 `knowhow_raw.md`/`crawl_config.json`/`seed_data.json` 三件套,',
'也不需要凑够 ≥50 条种子数据。只需要快速查一下下面这件事业界/公开资料里有没有值得参考的方法论,',
'给一段简短总结即可。',
'',
'## 调研主题',
topic || '(调用方没给主题)',
'',
'## 交回方式(只读你回复里最后一个 ```json 代码块)',
'```json',
'{"findings": "…(几句话总结,没查到就写清楚没查到)", "sources": ["url 或来源描述", "…"]}',
'```',
'查不到有用信息就如实说 `findings` 里没有可用结论、`sources` 给 `[]`——不要编。',
].join('\n');
}
async function defaultTriggerDeepResearch({ companyId, task, topic }) {
const AgentManager = require('../src/core/agent-manager');
const { extractJson } = require('./dream-service');
const manager = new AgentManager({}, path.resolve(__dirname, '../..'));
const agentName = resolveResearchAgentName(companyId);
let tokens = 0;
const onAcpEvent = (ev) => {
if (!ev || ev.method !== 'session/result') return;
const u = (ev.params && ev.params.usage) || null;
if (u) tokens += (u.input || 0) + (u.output || 0);
};
const res = await manager.callAgent(agentName, buildResearchPrompt({ topic }), {
onAcpEvent,
metadata: { taskId: task && task.id, layer: 'deep-research', companyId },
});
const obj = extractJson(res && res.output, 'object');
if (!obj || typeof obj.findings !== 'string' || !obj.findings.trim()) return null;
return {
findings: obj.findings.trim(),
sources: Array.isArray(obj.sources) ? obj.sources.filter((s) => typeof s === 'string') : [],
tokens,
};
}
module.exports = {
RESEARCH_AGENT_ID,
triggerDeepResearch,
setTriggerDeepResearch,
buildResearchPrompt,
resolveResearchAgentName,
};