agent-runtime.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. const crypto = require('crypto');
  2. const fs = require('fs');
  3. const path = require('path');
  4. const { spawn } = require('child_process');
  5. const { AgentContextBuilder, selectAuthoritativeHistory } = require('./agent-context-builder');
  6. const { planResponse } = require('./response-quality-planner');
  7. const { buildRewriteInstruction, verifyResponseQuality } = require('./response-quality-verifier');
  8. const { normalizeQualityChecks, normalizeQualityScore, qualityFallbackReason } = require('./response-quality-contract');
  9. const RISK_PATTERN = /合同|签约|产权|权属|资质|资格|政策|学区|入学|学位|合规|保证|承诺|最低价|成交价|报价|优惠|贷款|利率|税费|投诉|退款|赔偿|看房|预约|出价|面谈|付款|下单|发票|身份证|银行卡|法律|违约/;
  10. const NO_REPLY_NEEDED_PATTERN = /^(?:收到|好|好的|好嘞|明白|明白了|知道了|了解|了解了|谢谢|谢谢你|谢谢您|感谢|ok|okay|嗯+|哦+)$/i;
  11. function isNoReplyNeededMessage(input = '') {
  12. const normalized = String(input || '').trim().replace(/[,。!?!?,.~~]+$/g, '').trim();
  13. return Boolean(normalized && NO_REPLY_NEEDED_PATTERN.test(normalized));
  14. }
  15. function claudeSessionResetReason(error) {
  16. const detail = `${error?.message || ''} ${error?.subtype || ''}`;
  17. if (/error_max_budget_usd|max[-_\s]?budget[-_\s]?usd/i.test(detail)) return 'budget_exceeded';
  18. if (/session|conversation|resume/i.test(detail)) return 'session_invalid';
  19. return '';
  20. }
  21. function parseClaudeProcessResult(stdout = '', stderr = '', code = 0) {
  22. let payload = null;
  23. try { payload = JSON.parse(String(stdout || '').trim()); } catch {}
  24. if (code === 0) {
  25. return payload
  26. ? { payload }
  27. : { error: `Claude Code 未返回有效 JSON${stderr ? ',请检查 Fmode 配置' : ''}` };
  28. }
  29. const structured = payload?.structured_output;
  30. const usableStructuredOutput = structured
  31. && typeof structured === 'object'
  32. && !Array.isArray(structured)
  33. && Object.prototype.hasOwnProperty.call(structured, 'reply')
  34. && Object.prototype.hasOwnProperty.call(structured, 'confidence')
  35. && Object.prototype.hasOwnProperty.call(structured, 'intent');
  36. const safeError = String(stderr || '')
  37. .replace(/Bearer\s+\S+/gi, 'Bearer [REDACTED]')
  38. .replace(/\bsk-[A-Za-z0-9_-]{6,}\b/g, 'sk-[REDACTED]')
  39. .trim()
  40. .slice(0, 300);
  41. const structuredError = String(payload?.error || payload?.message || payload?.result || payload?.subtype || '').trim().slice(0, 300);
  42. const detail = safeError || structuredError || `退出码 ${code}`;
  43. if (usableStructuredOutput) {
  44. return {
  45. payload: {
  46. ...payload,
  47. is_error: false,
  48. process_warning: { exitCode: code, detail },
  49. },
  50. };
  51. }
  52. return { error: `Claude Code 调用失败(退出码 ${code})${detail ? `:${detail}` : ''}` };
  53. }
  54. class AgentNotConfiguredError extends Error {
  55. constructor() {
  56. super('Agent 模型尚未配置,消息已保留但不会生成伪造回复');
  57. this.name = 'AgentNotConfiguredError';
  58. }
  59. }
  60. class OpenAICompatibleClient {
  61. constructor(config) { this.config = config; }
  62. async complete(messages, tools) {
  63. if (!this.config.apiKey) throw new AgentNotConfiguredError();
  64. const body = {
  65. model: this.config.model,
  66. temperature: 0.2,
  67. messages,
  68. };
  69. if (Array.isArray(tools) && tools.length) {
  70. body.tools = tools;
  71. body.tool_choice = 'auto';
  72. }
  73. const response = await fetch(`${this.config.baseUrl}/chat/completions`, {
  74. method: 'POST',
  75. headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.config.apiKey}` },
  76. body: JSON.stringify(body),
  77. signal: AbortSignal.timeout(30000),
  78. });
  79. const payload = await response.json().catch(() => ({}));
  80. if (!response.ok) throw new Error(`Agent 上游暂时不可用(HTTP ${response.status})`);
  81. const message = payload.choices?.[0]?.message;
  82. if (!message) throw new Error('Agent 上游没有返回有效消息');
  83. return message;
  84. }
  85. }
  86. class AnthropicCompatibleClient {
  87. constructor(config) { this.config = config; }
  88. toAnthropicMessages(messages) {
  89. return messages.filter(message => message.role !== 'system').map(message => {
  90. if (message.role === 'tool') {
  91. return { role: 'user', content: [{ type: 'tool_result', tool_use_id: message.tool_call_id, content: message.content }] };
  92. }
  93. if (message.role === 'assistant' && message.tool_calls?.length) {
  94. const content = [];
  95. if (message.content) content.push({ type: 'text', text: message.content });
  96. for (const call of message.tool_calls) {
  97. let input = {};
  98. try { input = JSON.parse(call.function.arguments || '{}'); } catch {}
  99. content.push({ type: 'tool_use', id: call.id, name: call.function.name, input });
  100. }
  101. return { role: 'assistant', content };
  102. }
  103. return { role: message.role, content: message.content };
  104. });
  105. }
  106. async complete(messages, tools) {
  107. if (!this.config.apiKey) throw new AgentNotConfiguredError();
  108. const system = messages.find(message => message.role === 'system')?.content || '';
  109. const body = {
  110. model: this.config.model,
  111. max_tokens: 1400,
  112. temperature: 0.2,
  113. system,
  114. messages: this.toAnthropicMessages(messages),
  115. };
  116. if (Array.isArray(tools) && tools.length) {
  117. body.tools = tools.map(tool => ({
  118. name: tool.function.name,
  119. description: tool.function.description,
  120. input_schema: tool.function.parameters,
  121. }));
  122. }
  123. const response = await fetch(`${this.config.baseUrl}/v1/messages`, {
  124. method: 'POST',
  125. headers: {
  126. 'Content-Type': 'application/json',
  127. 'anthropic-version': '2023-06-01',
  128. 'x-api-key': this.config.apiKey,
  129. Authorization: `Bearer ${this.config.apiKey}`,
  130. },
  131. body: JSON.stringify(body),
  132. signal: AbortSignal.timeout(45000),
  133. });
  134. const payload = await response.json().catch(() => ({}));
  135. if (!response.ok) throw new Error(`Agent 上游暂时不可用(HTTP ${response.status})`);
  136. const blocks = Array.isArray(payload.content) ? payload.content : [];
  137. const toolCalls = blocks.filter(block => block.type === 'tool_use').map(block => ({
  138. id: block.id,
  139. function: { name: block.name, arguments: JSON.stringify(block.input || {}) },
  140. }));
  141. return {
  142. content: blocks.filter(block => block.type === 'text').map(block => block.text).join('\n'),
  143. ...(toolCalls.length ? { tool_calls: toolCalls } : {}),
  144. };
  145. }
  146. }
  147. function readJson(filePath, fallback = {}) {
  148. try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
  149. catch { return fallback; }
  150. }
  151. function writeJsonAtomic(filePath, value) {
  152. fs.mkdirSync(path.dirname(filePath), { recursive: true });
  153. const tempPath = `${filePath}.${process.pid}.tmp`;
  154. fs.writeFileSync(tempPath, JSON.stringify(value, null, 2), 'utf8');
  155. fs.renameSync(tempPath, filePath);
  156. }
  157. function resolveClaudeExecutable(config = {}) {
  158. const pathCandidates = String(process.env.PATH || '')
  159. .split(path.delimiter)
  160. .map(item => item.trim().replace(/^"|"$/g, ''))
  161. .filter(Boolean)
  162. .flatMap(dir => process.platform === 'win32'
  163. ? [
  164. path.join(dir, 'claude.exe'),
  165. path.join(dir, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
  166. ]
  167. : [path.join(dir, 'claude')]);
  168. const candidates = [
  169. config.claudeExecutable,
  170. process.env.CLAUDE_CODE_EXECUTABLE,
  171. ...pathCandidates,
  172. path.join(path.dirname(process.execPath), 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
  173. path.join(process.env.APPDATA || '', 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe'),
  174. ].filter(Boolean);
  175. return candidates.find(candidate => fs.existsSync(candidate)) || '';
  176. }
  177. function normalizeSessionLabel(value) {
  178. return String(value || '企微客户')
  179. .trim()
  180. .replace(/[\\/:*?"<>|\r\n]+/g, '-')
  181. .replace(/\s+/g, '-')
  182. .replace(/-+/g, '-')
  183. .replace(/^-|-$/g, '')
  184. .slice(0, 24) || '企微客户';
  185. }
  186. function buildClaudeSessionName(context = {}, key = '') {
  187. const customerName = normalizeSessionLabel(
  188. context.conversation?.contact_name || context.conversation?.displayName || '企微客户'
  189. );
  190. const reference = crypto.createHash('sha256').update(String(key || 'qiwei-default')).digest('hex').slice(0, 4);
  191. return `企微客户-${customerName}-${reference}`;
  192. }
  193. function uniqueIntelligence(items = [], keyFn) {
  194. const seen = new Set();
  195. return items.filter(item => {
  196. const key = keyFn(item);
  197. if (!key || seen.has(key)) return false;
  198. seen.add(key);
  199. return true;
  200. });
  201. }
  202. function normalizedEvidence(value) {
  203. return String(value || '').toLowerCase().replace(/[\s,。!?;:、,.!?;:'"“”‘’()()【】\[\]-]+/g, '');
  204. }
  205. function evidenceIsSupported(evidence, authoritativeText) {
  206. const source = normalizedEvidence(authoritativeText);
  207. const claim = normalizedEvidence(evidence);
  208. if (!source || !claim) return false;
  209. return source.includes(claim) || (source.length >= 4 && claim.includes(source));
  210. }
  211. function supportedModelProfileUpdates(updates = {}, authoritativeText = '') {
  212. if (!updates || typeof updates !== 'object' || Array.isArray(updates)) return {};
  213. const explicitNumbers = new Set(String(authoritativeText || '').match(/\d+(?:\.\d+)?/g) || []);
  214. return Object.fromEntries(Object.entries(updates).filter(([, value]) => {
  215. if (value === undefined || value === null || value === '') return false;
  216. const values = Array.isArray(value) ? value : [value];
  217. return values.every(item => typeof item === 'number'
  218. ? explicitNumbers.has(String(item))
  219. : evidenceIsSupported(String(item), authoritativeText));
  220. }));
  221. }
  222. function unsupportedAttributedClaims(final = {}, authoritativeHistory = []) {
  223. const output = `${String(final.reply || '')}\n${String(final.reason || '')}`;
  224. const source = authoritativeHistory.map(item => item.content || '').join('\n');
  225. const claims = [];
  226. const marker = /(?:您|客户)(?:之前|此前|刚才)?(?:曾经)?(?:提到|说过|说|表示)/g;
  227. for (const match of output.matchAll(marker)) {
  228. const tail = output.slice((match.index || 0) + match[0].length, (match.index || 0) + match[0].length + 100);
  229. const quoted = tail.match(/^\s*[::]?\s*["“‘']([^"”’'\r\n]{2,60})["”’']/);
  230. if (quoted && !evidenceIsSupported(quoted[1], source)) claims.push(quoted[1].trim());
  231. }
  232. return [...new Set(claims)];
  233. }
  234. function groundedConfirmationReply(profile = {}, inboundContent = '') {
  235. const budget = Number(profile.budgetWan || profile.budget || 0);
  236. const intent = profile.intent || profile.purpose || profile.need || '';
  237. const confirmed = [intent, budget ? `预算 ${budget}` : '', profile.timeline || ''].filter(Boolean).join('、');
  238. const opening = confirmed ? `收到,我先按${confirmed}继续整理。` : '收到,您刚才的信息我已经记录。';
  239. const questions = [];
  240. if (!intent) questions.push('这次最希望解决的核心问题是什么');
  241. if (budget && (!profile.budgetType || profile.budgetType === '待确认')) questions.push(`预算 ${budget} 是目标值还是上限`);
  242. if (!profile.timeline) questions.push('希望什么时候推进');
  243. if (!questions.length) return `${opening}我会先核对可用方案,再给您准确回复。`;
  244. return `${opening}为了避免理解偏差,想再确认一下:${questions.join(';')}?`;
  245. }
  246. function enforceAuthoritativeGrounding(final = {}, authoritativeHistory = [], profile = {}, inboundContent = '') {
  247. const unsupported = unsupportedAttributedClaims(final, authoritativeHistory);
  248. if (!unsupported.length) return { ...final, groundingWarnings: [] };
  249. return {
  250. ...final,
  251. reply: groundedConfirmationReply(profile, inboundContent),
  252. reason: `检测到模型引用了本轮有效会话中不存在的客户原话,已降级为确认式草稿。未支持内容:${unsupported.join('、')}`,
  253. confidence: Math.min(clamp(final.confidence), 0.68),
  254. requiresHuman: true,
  255. groundingWarnings: unsupported,
  256. };
  257. }
  258. function extractExplicitCustomerIntelligence(content, currentProfile = {}, modelOutput = {}) {
  259. const text = String(content || '').trim();
  260. const profileUpdates = supportedModelProfileUpdates(modelOutput.profileUpdates, text);
  261. const amount = text.match(/(\d+(?:\.\d+)?)\s*(?:万|元)?/);
  262. if (amount && (/预算|费用|报价|价格/.test(text))) {
  263. profileUpdates.budgetWan = Number(amount[1]);
  264. profileUpdates.budgetType = /上限|最多/.test(text) ? '上限' : /目标|大概/.test(text) ? '目标' : (currentProfile.budgetType || '待确认');
  265. }
  266. if (/今天|明天|本周|这周|尽快|马上|急/.test(text)) profileUpdates.urgency = '高';
  267. const mergedProfile = { ...currentProfile, ...profileUpdates };
  268. const factEvidence = [
  269. mergedProfile.intent || mergedProfile.purpose ? `意图${mergedProfile.intent || mergedProfile.purpose}` : '',
  270. mergedProfile.need || mergedProfile.needs ? `需求${mergedProfile.need || mergedProfile.needs}` : '',
  271. mergedProfile.budgetWan || mergedProfile.budget ? `预算${mergedProfile.budgetWan || mergedProfile.budget}` : '',
  272. mergedProfile.timeline ? `计划时间${mergedProfile.timeline}` : '',
  273. mergedProfile.decisionMaker ? `决策人${mergedProfile.decisionMaker}` : '',
  274. ].filter(Boolean).join(',').slice(0, 240);
  275. const evidence = text.slice(0, 240);
  276. const tasks = Array.isArray(modelOutput.tasks)
  277. ? modelOutput.tasks.filter(item => evidenceIsSupported(item?.evidence, text)).map(item => ({
  278. ...item,
  279. businessKey: item.businessKey || item.key,
  280. managedBy: 'agent',
  281. }))
  282. : [];
  283. const alerts = Array.isArray(modelOutput.alerts)
  284. ? modelOutput.alerts.filter(item => evidenceIsSupported(item?.evidence, text)).map(item => ({
  285. ...item,
  286. businessKey: item.businessKey || item.key,
  287. managedBy: 'agent',
  288. }))
  289. : [];
  290. const hasBudget = Number(mergedProfile.budgetWan || mergedProfile.budget || 0) > 0;
  291. const hasPurpose = Boolean(mergedProfile.intent || mergedProfile.purpose || mergedProfile.need || mergedProfile.needs);
  292. const hasTimeline = Boolean(mergedProfile.timeline);
  293. if ((hasBudget || hasPurpose) && (!hasPurpose || !hasTimeline)) {
  294. tasks.push({
  295. businessKey: 'qualification:purpose_and_timeline',
  296. managedBy: 'rule',
  297. type: 'qualification',
  298. title: '确认客户目标与计划时间',
  299. owner: '待分配',
  300. dueAt: '',
  301. priority: 'high',
  302. reason: `${[!hasPurpose ? '客户目标' : '', !hasTimeline ? '计划时间' : ''].filter(Boolean).join('和')}仍不明确。`,
  303. evidence: factEvidence || evidence,
  304. });
  305. }
  306. if (hasPurpose && hasTimeline) {
  307. alerts.push({
  308. businessKey: 'high_intent:core_demand_ready',
  309. managedBy: 'rule',
  310. type: 'high_intent',
  311. severity: 'high',
  312. title: '客户核心需求已基本成形',
  313. detail: '客户目标和计划时间已具备,可以从泛咨询进入下一步服务安排。',
  314. evidence: factEvidence || evidence,
  315. recommendedAction: '核对关键约束并确认下一步动作。',
  316. });
  317. }
  318. if (/投诉|不满意|骗人|退款|举报|再也不|太差|生气/.test(text)) {
  319. alerts.push({
  320. businessKey: 'complaint:manual_takeover',
  321. managedBy: 'event',
  322. type: 'complaint',
  323. severity: 'critical',
  324. title: '检测到投诉或强烈负面情绪',
  325. detail: '该消息不应由自动回复独立处理。',
  326. evidence,
  327. recommendedAction: '立即人工接管,先确认事实和客户诉求。',
  328. });
  329. } else if (/今天|明天|本周|这周|尽快|马上|急/.test(text)) {
  330. alerts.push({
  331. businessKey: 'time_sensitive:follow_up',
  332. managedBy: 'event',
  333. type: 'time_sensitive',
  334. severity: 'high',
  335. title: '客户表达了明确时效要求',
  336. detail: '消息中包含较紧迫的时间表达。',
  337. evidence,
  338. recommendedAction: '优先确认具体截止时间并安排跟进。',
  339. });
  340. }
  341. return {
  342. profileUpdates,
  343. tasks: uniqueIntelligence(tasks, item => `${item?.businessKey || item?.key || ''}|${item?.type || ''}|${item?.title || ''}`.toLowerCase()).slice(0, 8),
  344. alerts: uniqueIntelligence(alerts, item => `${item?.businessKey || item?.key || ''}|${item?.type || ''}|${item?.title || ''}`.toLowerCase()).slice(0, 6),
  345. };
  346. }
  347. class ClaudeCodeSessionStore {
  348. constructor(filePath, project = {}) {
  349. this.filePath = filePath;
  350. this.project = project;
  351. }
  352. loadState() {
  353. const state = readJson(this.filePath, { version: 1, project: {}, sessions: {} });
  354. state.version = 1;
  355. state.sessions ||= {};
  356. state.project ||= {};
  357. state.project.projectId = this.project.projectId || state.project.projectId || 'qiwei-project';
  358. state.project.projectRoot = this.project.projectRoot || state.project.projectRoot || '';
  359. state.project.controllerSessionId = this.project.mainSessionId
  360. || state.project.controllerSessionId
  361. || crypto.randomUUID();
  362. state.project.boundMainSessionId = this.project.mainSessionId || state.project.boundMainSessionId || null;
  363. return state;
  364. }
  365. get(key) {
  366. return this.loadState().sessions?.[key] || null;
  367. }
  368. ensure(key, metadata = {}) {
  369. const state = this.loadState();
  370. if (!state.sessions[key]) {
  371. state.sessions[key] = {
  372. id: crypto.randomUUID(),
  373. initialized: false,
  374. role: 'customer-agent',
  375. projectId: state.project.projectId,
  376. parentControllerSessionId: state.project.controllerSessionId,
  377. epochStartedAt: new Date().toISOString(),
  378. epochTurnCount: 0,
  379. memoryVersion: Number(metadata.memoryVersion || 0),
  380. createdAt: new Date().toISOString(),
  381. };
  382. }
  383. const session = state.sessions[key];
  384. session.role ||= 'customer-agent';
  385. session.projectId ||= state.project.projectId;
  386. session.parentControllerSessionId ||= state.project.controllerSessionId;
  387. if (metadata.customerName) session.customerName = String(metadata.customerName).trim().slice(0, 80);
  388. if (metadata.displayName) session.displayName = String(metadata.displayName).trim().slice(0, 80);
  389. if (metadata.memoryVersion !== undefined) session.latestMemoryVersion = Number(metadata.memoryVersion || 0);
  390. writeJsonAtomic(this.filePath, state);
  391. return session;
  392. }
  393. markInitialized(key, metadata = {}) {
  394. const state = this.loadState();
  395. if (!state.sessions?.[key]) return;
  396. state.sessions[key].initialized = true;
  397. state.sessions[key].epochTurnCount = Number(state.sessions[key].epochTurnCount || 0) + 1;
  398. if (metadata.memoryVersion !== undefined) state.sessions[key].latestMemoryVersion = Number(metadata.memoryVersion || 0);
  399. state.sessions[key].updatedAt = new Date().toISOString();
  400. writeJsonAtomic(this.filePath, state);
  401. }
  402. setRole(key, role) {
  403. const state = this.loadState();
  404. if (!state.sessions?.[key]) return false;
  405. state.sessions[key].role = String(role || 'customer-agent');
  406. state.sessions[key].updatedAt = new Date().toISOString();
  407. writeJsonAtomic(this.filePath, state);
  408. return true;
  409. }
  410. reset(key, metadata = {}) {
  411. const state = this.loadState();
  412. const previous = state.sessions[key] || null;
  413. const epochHistory = previous ? [
  414. ...(previous.epochHistory || []),
  415. {
  416. id: previous.id,
  417. startedAt: previous.epochStartedAt || previous.createdAt,
  418. endedAt: new Date().toISOString(),
  419. turnCount: Number(previous.epochTurnCount || 0),
  420. memoryVersion: Number(previous.memoryVersion || 0),
  421. latestMemoryVersion: Number(previous.latestMemoryVersion || previous.memoryVersion || 0),
  422. closedReason: metadata.closedReason || 'manual_reset',
  423. },
  424. ].slice(-50) : [];
  425. state.sessions[key] = {
  426. id: crypto.randomUUID(),
  427. initialized: false,
  428. role: 'customer-agent',
  429. projectId: state.project.projectId,
  430. parentControllerSessionId: state.project.controllerSessionId,
  431. parentSessionId: metadata.parentSessionId || previous?.id || undefined,
  432. closedReason: metadata.closedReason || undefined,
  433. epochStartedAt: new Date().toISOString(),
  434. epochTurnCount: 0,
  435. memoryVersion: Number(metadata.memoryVersion || 0),
  436. epochHistory,
  437. customerName: metadata.customerName || undefined,
  438. displayName: metadata.displayName || undefined,
  439. createdAt: new Date().toISOString(),
  440. };
  441. writeJsonAtomic(this.filePath, state);
  442. return state.sessions[key];
  443. }
  444. rotateIfNeeded(key, metadata = {}, policy = {}) {
  445. const session = this.ensure(key, metadata);
  446. if (!session.initialized) return { session, reason: '' };
  447. const maxTurns = Number(policy.maxTurns || 0);
  448. const maxAgeMs = Number(policy.maxAgeMs || 0);
  449. const ageMs = Date.now() - Date.parse(session.epochStartedAt || session.createdAt || 0);
  450. const reason = maxTurns > 0 && Number(session.epochTurnCount || 0) >= maxTurns
  451. ? 'epoch_turn_limit'
  452. : maxAgeMs > 0 && Number.isFinite(ageMs) && ageMs >= maxAgeMs
  453. ? 'epoch_age_limit'
  454. : '';
  455. if (!reason) return { session, reason: '' };
  456. return {
  457. session: this.reset(key, { ...metadata, parentSessionId: session.id, closedReason: reason }),
  458. reason,
  459. };
  460. }
  461. }
  462. class ClaudeCodeClient {
  463. constructor(config, contextBuilder = null) {
  464. this.config = config;
  465. this.executable = resolveClaudeExecutable(config);
  466. this.workdir = path.resolve(config.claudeWorkdir || process.cwd());
  467. this.sessionStore = new ClaudeCodeSessionStore(config.claudeSessionFile, {
  468. projectId: config.claudeProjectId,
  469. projectRoot: this.workdir,
  470. mainSessionId: config.claudeMainSessionId,
  471. });
  472. this.queues = new Map();
  473. this.contextBuilder = contextBuilder || new AgentContextBuilder({ config });
  474. }
  475. isConfigured() {
  476. return Boolean(this.executable && fs.existsSync(this.workdir));
  477. }
  478. outputSchema() {
  479. return this.config.outputSchema || {
  480. type: 'object',
  481. additionalProperties: false,
  482. properties: {
  483. reply: { type: 'string' },
  484. confidence: { type: 'number', minimum: 0, maximum: 1 },
  485. intent: { type: 'string' },
  486. reason: { type: 'string' },
  487. requiresHuman: { type: 'boolean' },
  488. profileUpdates: { type: 'object' },
  489. tasks: {
  490. type: 'array',
  491. maxItems: 6,
  492. items: {
  493. type: 'object',
  494. additionalProperties: false,
  495. properties: {
  496. key: { type: 'string' },
  497. type: { type: 'string' },
  498. title: { type: 'string' },
  499. owner: { type: 'string' },
  500. dueAt: { type: 'string' },
  501. priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
  502. reason: { type: 'string' },
  503. evidence: { type: 'string' },
  504. },
  505. required: ['type', 'title', 'owner', 'dueAt', 'priority', 'reason', 'evidence'],
  506. },
  507. },
  508. alerts: {
  509. type: 'array',
  510. maxItems: 4,
  511. items: {
  512. type: 'object',
  513. additionalProperties: false,
  514. properties: {
  515. key: { type: 'string' },
  516. type: { type: 'string' },
  517. severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
  518. title: { type: 'string' },
  519. detail: { type: 'string' },
  520. evidence: { type: 'string' },
  521. recommendedAction: { type: 'string' },
  522. },
  523. required: ['type', 'severity', 'title', 'detail', 'evidence', 'recommendedAction'],
  524. },
  525. },
  526. },
  527. required: ['reply', 'confidence', 'intent', 'reason', 'requiresHuman', 'profileUpdates', 'tasks', 'alerts'],
  528. };
  529. }
  530. sessionKey(context = {}) {
  531. return String(context.conversation?.id || context.conversation?.contact_id || 'qiwei-default');
  532. }
  533. buildPrompt(messages, context = {}) {
  534. return this.contextBuilder.buildClaudePrompt(messages, context);
  535. }
  536. runProcess(args, input = '') {
  537. if (!this.isConfigured()) throw new AgentNotConfiguredError();
  538. return new Promise((resolve, reject) => {
  539. const childEnv = { ...process.env, NO_COLOR: '1' };
  540. if (this.config.claudeBare !== false && childEnv.ANTHROPIC_AUTH_TOKEN) {
  541. childEnv.ANTHROPIC_API_KEY = childEnv.ANTHROPIC_AUTH_TOKEN;
  542. }
  543. const child = spawn(this.executable, args, {
  544. cwd: this.workdir,
  545. env: childEnv,
  546. windowsHide: true,
  547. stdio: ['pipe', 'pipe', 'pipe'],
  548. });
  549. let stdout = '';
  550. let stderr = '';
  551. let finished = false;
  552. const maxBuffer = 8 * 1024 * 1024;
  553. const timer = setTimeout(() => {
  554. if (finished) return;
  555. child.kill('SIGTERM');
  556. reject(new Error('Claude Code 处理超时,已转人工审核'));
  557. }, Number(this.config.claudeTimeoutMs || 120000));
  558. child.stdout.on('data', chunk => {
  559. stdout += chunk.toString('utf8');
  560. if (stdout.length > maxBuffer) child.kill('SIGTERM');
  561. });
  562. child.stderr.on('data', chunk => {
  563. stderr += chunk.toString('utf8');
  564. if (stderr.length > maxBuffer) child.kill('SIGTERM');
  565. });
  566. child.stdin.on('error', () => {});
  567. child.stdin.end(String(input || ''), 'utf8');
  568. child.once('error', error => {
  569. if (finished) return;
  570. finished = true;
  571. clearTimeout(timer);
  572. reject(error);
  573. });
  574. child.once('close', code => {
  575. if (finished) return;
  576. finished = true;
  577. clearTimeout(timer);
  578. const parsed = parseClaudeProcessResult(stdout, stderr, code);
  579. if (parsed.error) reject(new Error(parsed.error));
  580. else resolve(parsed.payload);
  581. });
  582. });
  583. }
  584. async invoke(messages, context, session, options = {}) {
  585. const system = messages.find(message => message.role === 'system')?.content || '';
  586. const businessPrompt = this.buildPrompt(messages, context);
  587. const prompt = `${system}\n\n${businessPrompt}`;
  588. const budgetLimitUsd = Number(options.maxBudgetUsd || this.config.claudeMaxBudgetUsd || 1);
  589. const args = [
  590. '--print',
  591. '--output-format', 'json',
  592. '--permission-mode', 'dontAsk',
  593. ...(this.config.claudeBare !== false ? ['--bare'] : []),
  594. ...(this.config.claudeEffort ? ['--effort', String(this.config.claudeEffort)] : []),
  595. '--tools', String(this.config.claudeTools || 'Read,Glob,Grep'),
  596. '--model', String(this.config.model || 'sonnet'),
  597. '--max-budget-usd', String(budgetLimitUsd),
  598. '--json-schema', JSON.stringify(this.outputSchema()),
  599. '--name', session.displayName || buildClaudeSessionName(context, this.sessionKey(context)),
  600. ];
  601. for (const dir of this.config.claudeAddDirs || []) {
  602. if (dir && fs.existsSync(dir)) args.push('--add-dir', path.resolve(dir));
  603. }
  604. if (session.initialized) args.push('--resume', session.id);
  605. else args.push('--session-id', session.id);
  606. const payload = await this.runProcess(args, prompt);
  607. if (payload.is_error) {
  608. const subtype = String(payload.subtype || 'unknown');
  609. const error = new Error(`Claude Code/Fmode 暂时不可用(${subtype})`);
  610. error.subtype = subtype;
  611. throw error;
  612. }
  613. const structured = payload.structured_output ?? payload.result;
  614. if (structured === undefined || structured === null || structured === '') {
  615. throw new Error('Claude Code 没有返回客服草稿');
  616. }
  617. return {
  618. content: typeof structured === 'string' ? structured : JSON.stringify(structured),
  619. claudeCode: {
  620. model: this.config.model,
  621. sessionName: session.displayName || buildClaudeSessionName(context, this.sessionKey(context)),
  622. durationMs: Number(payload.duration_ms || 0),
  623. costUsd: Number(payload.total_cost_usd || 0),
  624. budgetLimitUsd,
  625. resumed: Boolean(session.initialized),
  626. systemChars: system.length,
  627. businessPromptChars: businessPrompt.length,
  628. promptChars: prompt.length,
  629. ...(payload.process_warning ? { processWarning: payload.process_warning } : {}),
  630. },
  631. };
  632. }
  633. async complete(messages, tools, context = {}) {
  634. const key = this.sessionKey(context);
  635. const metadata = {
  636. customerName: context.conversation?.contact_name || context.conversation?.displayName || '',
  637. displayName: buildClaudeSessionName(context, key),
  638. memoryVersion: Number(context.memoryContext?.snapshot?.version || 0),
  639. };
  640. const previous = this.queues.get(key) || Promise.resolve();
  641. const current = previous.catch(() => {}).then(async () => {
  642. const rotated = this.sessionStore.rotateIfNeeded(key, metadata, {
  643. maxTurns: this.config.claudeSessionMaxTurns,
  644. maxAgeMs: this.config.claudeSessionMaxAgeMs,
  645. });
  646. let session = rotated.session;
  647. try {
  648. const result = await this.invoke(messages, context, session);
  649. this.sessionStore.markInitialized(key, metadata);
  650. if (rotated.reason && result.claudeCode) result.claudeCode.sessionResetReason = rotated.reason;
  651. return result;
  652. } catch (error) {
  653. const resetReason = claudeSessionResetReason(error);
  654. if (resetReason) {
  655. session = this.sessionStore.reset(key, { ...metadata, parentSessionId: session.id, closedReason: resetReason });
  656. const retryMaxBudgetUsd = resetReason === 'budget_exceeded'
  657. ? Number(this.config.claudeRetryMaxBudgetUsd || Math.max(Number(this.config.claudeMaxBudgetUsd || 1) * 2, 3))
  658. : undefined;
  659. const result = await this.invoke(messages, context, session, { maxBudgetUsd: retryMaxBudgetUsd });
  660. this.sessionStore.markInitialized(key, metadata);
  661. if (result.claudeCode) result.claudeCode.sessionResetReason = resetReason;
  662. return result;
  663. }
  664. throw error;
  665. }
  666. });
  667. this.queues.set(key, current);
  668. try { return await current; }
  669. finally { if (this.queues.get(key) === current) this.queues.delete(key); }
  670. }
  671. }
  672. function parseJsonCandidate(value) {
  673. if (value && typeof value === 'object' && !Array.isArray(value)) return value;
  674. const text = String(value || '').trim();
  675. if (!text) return null;
  676. try {
  677. const parsed = JSON.parse(text);
  678. return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
  679. } catch {}
  680. const fenced = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/gi)];
  681. for (const match of fenced) {
  682. try {
  683. const parsed = JSON.parse(match[1].trim());
  684. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
  685. } catch {}
  686. }
  687. for (let start = text.indexOf('{'); start >= 0; start = text.indexOf('{', start + 1)) {
  688. let depth = 0;
  689. let inString = false;
  690. let escaped = false;
  691. for (let index = start; index < text.length; index += 1) {
  692. const char = text[index];
  693. if (inString) {
  694. if (escaped) escaped = false;
  695. else if (char === '\\') escaped = true;
  696. else if (char === '"') inString = false;
  697. continue;
  698. }
  699. if (char === '"') inString = true;
  700. else if (char === '{') depth += 1;
  701. else if (char === '}') {
  702. depth -= 1;
  703. if (depth === 0) {
  704. try {
  705. const parsed = JSON.parse(text.slice(start, index + 1));
  706. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
  707. } catch {}
  708. break;
  709. }
  710. }
  711. }
  712. }
  713. return null;
  714. }
  715. function parseFinal(content) {
  716. const text = String(content || '').trim();
  717. let parsed = parseJsonCandidate(text);
  718. for (let depth = 0; parsed && depth < 3; depth += 1) {
  719. const wrapped = !Object.prototype.hasOwnProperty.call(parsed, 'reply')
  720. ? parseJsonCandidate(parsed.structured_output) || parseJsonCandidate(parsed.result)
  721. : null;
  722. if (wrapped) {
  723. parsed = wrapped;
  724. continue;
  725. }
  726. const nestedReply = parseJsonCandidate(parsed.reply);
  727. if (!nestedReply || !Object.prototype.hasOwnProperty.call(nestedReply, 'reply')) break;
  728. parsed = { ...parsed, ...nestedReply };
  729. }
  730. if (parsed && Object.prototype.hasOwnProperty.call(parsed, 'reply')) return parsed;
  731. const looksStructured = /^\s*[\[{]/.test(text) || /```(?:json)?/i.test(text) || /["']reply["']\s*:/.test(text);
  732. return {
  733. reply: looksStructured ? '' : text,
  734. confidence: 0.5,
  735. intent: 'unknown',
  736. reason: looksStructured
  737. ? '模型返回的结构化结果无法安全解析,已阻止 JSON 进入客户回复'
  738. : '模型未返回结构化结果,必须人工审核',
  739. requiresHuman: true,
  740. profileUpdates: {},
  741. };
  742. }
  743. function clamp(value) { return Math.max(0, Math.min(1, Number(value) || 0)); }
  744. class QiweiAgentRuntime {
  745. constructor({ config, knowledge, modelClient = null }) {
  746. this.config = config;
  747. this.knowledge = knowledge;
  748. this.contextBuilder = new AgentContextBuilder({ config, knowledge });
  749. this.modelClient = modelClient || (config.provider === 'claude-code'
  750. ? new ClaudeCodeClient(config, this.contextBuilder)
  751. : config.provider === 'anthropic'
  752. ? new AnthropicCompatibleClient(config)
  753. : new OpenAICompatibleClient(config));
  754. }
  755. tools() {
  756. return [
  757. {
  758. type: 'function',
  759. function: {
  760. name: 'search_knowledge',
  761. description: '检索企业规则、FAQ 和沟通 Playbook。',
  762. parameters: {
  763. type: 'object',
  764. properties: { query: { type: 'string' }, limit: { type: 'integer' } },
  765. required: ['query'],
  766. },
  767. },
  768. },
  769. {
  770. type: 'function',
  771. function: {
  772. name: 'get_customer_profile',
  773. description: '读取当前客户画像与标签。',
  774. parameters: { type: 'object', properties: {} },
  775. },
  776. },
  777. ];
  778. }
  779. async executeTool(name, args, context) {
  780. if (name === 'search_knowledge') return this.knowledge.search(args.query, args.limit || 5);
  781. if (name === 'get_customer_profile') return context.profile || { profile: {}, tags: [] };
  782. return { error: `未知工具 ${name}` };
  783. }
  784. async run({ conversation, messages, profile, customerIntelligence = {}, inboundContent, channelType = 'private', directPrompt = '', memoryContext = {} }) {
  785. const history = messages.slice(-16).map(message => ({
  786. role: message.direction === 'inbound' ? 'user' : 'assistant',
  787. content: message.content,
  788. }));
  789. const system = this.contextBuilder.buildSystemContext({ channelType, customerIntelligence, memoryContext });
  790. const llmMessages = [{ role: 'system', content: system }, ...history];
  791. const toolTrace = [];
  792. const citations = [];
  793. for (let round = 0; round < this.config.maxToolRounds; round += 1) {
  794. const assistant = await this.modelClient.complete(llmMessages, this.tools(), {
  795. conversation,
  796. profile,
  797. customerIntelligence,
  798. inboundContent,
  799. channelType,
  800. directPrompt,
  801. memoryContext,
  802. });
  803. if (assistant.claudeCode) {
  804. toolTrace.push({
  805. tool: 'claude_code_session',
  806. args: { provider: 'Fmode Studio', model: assistant.claudeCode.model },
  807. result: {
  808. sessionName: assistant.claudeCode.sessionName,
  809. durationMs: assistant.claudeCode.durationMs,
  810. costUsd: assistant.claudeCode.costUsd,
  811. budgetLimitUsd: assistant.claudeCode.budgetLimitUsd,
  812. resumed: assistant.claudeCode.resumed,
  813. systemChars: assistant.claudeCode.systemChars,
  814. businessPromptChars: assistant.claudeCode.businessPromptChars,
  815. promptChars: assistant.claudeCode.promptChars,
  816. ...(assistant.claudeCode.processWarning ? { processWarning: assistant.claudeCode.processWarning } : {}),
  817. ...(assistant.claudeCode.sessionResetReason ? { sessionResetReason: assistant.claudeCode.sessionResetReason } : {}),
  818. },
  819. });
  820. }
  821. if (assistant.tool_calls?.length) {
  822. llmMessages.push({ role: 'assistant', content: assistant.content || '', tool_calls: assistant.tool_calls });
  823. for (const call of assistant.tool_calls) {
  824. let args = {};
  825. try { args = JSON.parse(call.function.arguments || '{}'); } catch {}
  826. const result = await this.executeTool(call.function.name, args, { conversation, profile, customerIntelligence });
  827. toolTrace.push({ tool: call.function.name, args, result });
  828. if (call.function.name === 'search_knowledge') {
  829. for (const item of result) citations.push({ id: item.id, source: item.source, heading: item.heading });
  830. }
  831. llmMessages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
  832. }
  833. continue;
  834. }
  835. const authoritativeHistory = selectAuthoritativeHistory(history, 10);
  836. const currentProfile = profile?.profile || profile || {};
  837. const responsePlan = planResponse({
  838. inboundContent,
  839. messages: history,
  840. profile: currentProfile,
  841. customerIntelligence,
  842. toolTrace,
  843. citations,
  844. businessGoal: this.config.businessGoal,
  845. stagePlaybook: this.config.stagePlaybook,
  846. });
  847. const assess = content => {
  848. const parsed = parseFinal(content);
  849. const grounded = enforceAuthoritativeGrounding(parsed, authoritativeHistory, currentProfile, inboundContent);
  850. const quality = verifyResponseQuality({
  851. reply: grounded.reply,
  852. inboundContent,
  853. messages: history,
  854. profile: currentProfile,
  855. customerIntelligence,
  856. citations,
  857. toolTrace,
  858. plan: responsePlan,
  859. requiresHuman: Boolean(grounded.requiresHuman),
  860. passScore: this.config.qualityPassScore,
  861. });
  862. return { final: grounded, quality };
  863. };
  864. let candidate = assess(assistant.content);
  865. let rewriteCount = 0;
  866. toolTrace.push({
  867. tool: 'response_quality_gate',
  868. args: { phase: 'evaluate', version: candidate.quality.version },
  869. result: {
  870. passed: candidate.quality.passed,
  871. score: candidate.quality.score,
  872. failedCheckIds: candidate.quality.failedCheckIds,
  873. hardFailureIds: candidate.quality.hardFailureIds,
  874. },
  875. });
  876. if (!candidate.quality.passed) {
  877. rewriteCount = 1;
  878. const qualityRepair = buildRewriteInstruction(candidate.quality);
  879. const repairMessages = llmMessages.map((message, index) => index === 0 && message.role === 'system'
  880. ? { ...message, content: `${message.content}\n\n【生成后质量修订】\n${qualityRepair}` }
  881. : message);
  882. try {
  883. const revised = await this.modelClient.complete(repairMessages, [], {
  884. conversation,
  885. profile,
  886. customerIntelligence,
  887. inboundContent,
  888. channelType,
  889. directPrompt,
  890. memoryContext,
  891. qualityRepair,
  892. rewriteAttempt: 1,
  893. });
  894. if (revised.claudeCode) {
  895. toolTrace.push({
  896. tool: 'claude_code_session',
  897. args: { provider: 'Fmode Studio', model: revised.claudeCode.model, phase: 'quality_rewrite' },
  898. result: {
  899. sessionName: revised.claudeCode.sessionName,
  900. durationMs: revised.claudeCode.durationMs,
  901. costUsd: revised.claudeCode.costUsd,
  902. budgetLimitUsd: revised.claudeCode.budgetLimitUsd,
  903. resumed: revised.claudeCode.resumed,
  904. systemChars: revised.claudeCode.systemChars,
  905. businessPromptChars: revised.claudeCode.businessPromptChars,
  906. promptChars: revised.claudeCode.promptChars,
  907. },
  908. });
  909. }
  910. candidate = assess(revised.content);
  911. if (!candidate.quality.passed) {
  912. candidate.quality.fallbackReason = qualityFallbackReason({
  913. qualityPassed: false,
  914. rewriteCount,
  915. });
  916. }
  917. } catch (error) {
  918. const rewriteCheck = {
  919. id: 'rewrite_failed',
  920. passed: false,
  921. weight: 0,
  922. hard: true,
  923. detail: '质量重写执行失败',
  924. };
  925. candidate.quality = {
  926. ...candidate.quality,
  927. passed: false,
  928. failedCheckIds: [...new Set([...candidate.quality.failedCheckIds, 'rewrite_failed'])],
  929. hardFailureIds: [...new Set([...(candidate.quality.hardFailureIds || []), 'rewrite_failed'])],
  930. checks: [...(candidate.quality.checks || []), rewriteCheck],
  931. fallbackReason: qualityFallbackReason({ qualityPassed: false, rewriteCount }),
  932. };
  933. toolTrace.push({
  934. tool: 'response_quality_rewrite',
  935. args: { phase: 'recover' },
  936. result: { status: 'failed', errorType: error?.name || 'Error' },
  937. });
  938. }
  939. toolTrace.push({
  940. tool: 'response_quality_gate',
  941. args: { phase: 'rewrite_evaluate', version: candidate.quality.version },
  942. result: {
  943. passed: candidate.quality.passed,
  944. score: candidate.quality.score,
  945. failedCheckIds: candidate.quality.failedCheckIds,
  946. hardFailureIds: candidate.quality.hardFailureIds,
  947. },
  948. });
  949. }
  950. const final = candidate.final;
  951. const intelligence = extractExplicitCustomerIntelligence(inboundContent, currentProfile, final);
  952. const risky = RISK_PATTERN.test(inboundContent) || RISK_PATTERN.test(final.reply || '');
  953. const confidence = risky ? Math.min(clamp(final.confidence), 0.75) : clamp(final.confidence);
  954. const requiresHuman = Boolean(final.requiresHuman || responsePlan.requireHumanHandoff || risky || confidence < 0.7 || !candidate.quality.passed);
  955. return {
  956. content: String(final.reply || '').trim(),
  957. confidence,
  958. intent: String(final.intent || 'unknown'),
  959. reason: String(final.reason || 'Agent 未提供说明'),
  960. requiresHuman,
  961. profileUpdates: intelligence.profileUpdates,
  962. tasks: intelligence.tasks,
  963. alerts: intelligence.alerts,
  964. citations: [...new Map(citations.map(item => [item.id, item])).values()],
  965. toolTrace,
  966. qualityScore: normalizeQualityScore(candidate.quality.score),
  967. qualityChecks: normalizeQualityChecks(candidate.quality.checks),
  968. qualityPassed: candidate.quality.passed,
  969. qualityPlan: candidate.quality.plan,
  970. rewriteCount,
  971. fallbackReason: qualityFallbackReason({
  972. qualityAvailable: candidate.quality?.score !== null && candidate.quality?.score !== undefined,
  973. qualityPassed: candidate.quality.passed,
  974. rewriteCount,
  975. requiresHuman,
  976. }),
  977. };
  978. }
  979. throw new Error('Agent 工具调用轮次超过上限,已转人工处理');
  980. }
  981. }
  982. module.exports = {
  983. AgentNotConfiguredError,
  984. OpenAICompatibleClient,
  985. AnthropicCompatibleClient,
  986. ClaudeCodeClient,
  987. ClaudeCodeSessionStore,
  988. buildClaudeSessionName,
  989. claudeSessionResetReason,
  990. parseClaudeProcessResult,
  991. parseFinal,
  992. selectAuthoritativeHistory,
  993. enforceAuthoritativeGrounding,
  994. extractExplicitCustomerIntelligence,
  995. isNoReplyNeededMessage,
  996. resolveClaudeExecutable,
  997. QiweiAgentRuntime,
  998. };