/** * 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, };