@yeaft/webchat-agent 0.0.166 → 0.0.168
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/claude.js +1 -1
- package/connection.js +2 -1
- package/crew.js +180 -4
- package/package.json +1 -1
package/claude.js
CHANGED
|
@@ -364,7 +364,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
364
364
|
|
|
365
365
|
// 计算上下文使用百分比
|
|
366
366
|
const inputTokens = message.usage?.input_tokens || 0;
|
|
367
|
-
const maxContextTokens =
|
|
367
|
+
const maxContextTokens = 128000; // API max_prompt_tokens 限制
|
|
368
368
|
if (inputTokens > 0) {
|
|
369
369
|
ctx.sendToServer({
|
|
370
370
|
type: 'context_usage',
|
package/connection.js
CHANGED
|
@@ -33,7 +33,8 @@ const BUFFERABLE_TYPES = new Set([
|
|
|
33
33
|
'background_task_started', 'background_task_output',
|
|
34
34
|
'crew_output', 'crew_status', 'crew_turn_completed',
|
|
35
35
|
'crew_session_created', 'crew_session_restored', 'crew_human_needed',
|
|
36
|
-
'crew_role_added', 'crew_role_removed'
|
|
36
|
+
'crew_role_added', 'crew_role_removed',
|
|
37
|
+
'crew_role_compact', 'crew_context_usage'
|
|
37
38
|
]);
|
|
38
39
|
|
|
39
40
|
// Send message to server (with encryption if available)
|
package/crew.js
CHANGED
|
@@ -1066,7 +1066,7 @@ async function clearRoleSessionId(sharedDir, roleName) {
|
|
|
1066
1066
|
function classifyRoleError(error) {
|
|
1067
1067
|
const msg = error.message || '';
|
|
1068
1068
|
if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
|
|
1069
|
-
return { recoverable: true, reason: 'context_exceeded', skipResume: true };
|
|
1069
|
+
return { recoverable: true, reason: 'context_exceeded', skipResume: true, needContentTrim: true };
|
|
1070
1070
|
}
|
|
1071
1071
|
if (/compact|compress|context.*reduc/i.test(msg)) {
|
|
1072
1072
|
return { recoverable: true, reason: 'compact_failed', skipResume: true };
|
|
@@ -1083,6 +1083,22 @@ function classifyRoleError(error) {
|
|
|
1083
1083
|
return { recoverable: true, reason: 'unknown', skipResume: false };
|
|
1084
1084
|
}
|
|
1085
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* context 超限时精简重派内容
|
|
1088
|
+
*/
|
|
1089
|
+
function trimContentForRetry(content) {
|
|
1090
|
+
if (typeof content === 'string' && content.length > 2000) {
|
|
1091
|
+
return `[注意: 上一轮因 context 超限被截断重发]\n\n${content.substring(0, 2000)}\n\n[内容已截断,请基于已知信息继续工作]`;
|
|
1092
|
+
}
|
|
1093
|
+
if (Array.isArray(content)) {
|
|
1094
|
+
return content.filter(b => b.type === 'text').map(b => ({
|
|
1095
|
+
...b,
|
|
1096
|
+
text: b.text.length > 2000 ? b.text.substring(0, 2000) + '\n[已截断]' : b.text
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
return content;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1086
1102
|
// =====================================================================
|
|
1087
1103
|
// Role Query Management
|
|
1088
1104
|
// =====================================================================
|
|
@@ -1142,7 +1158,12 @@ async function createRoleQuery(session, roleName) {
|
|
|
1142
1158
|
lastDispatchContent: null,
|
|
1143
1159
|
lastDispatchFrom: null,
|
|
1144
1160
|
lastDispatchTaskId: null,
|
|
1145
|
-
lastDispatchTaskTitle: null
|
|
1161
|
+
lastDispatchTaskTitle: null,
|
|
1162
|
+
// compact 状态
|
|
1163
|
+
_compacting: false, // 是否正在 compact
|
|
1164
|
+
_compactSummaryPending: false, // 是否等待过滤 compact summary
|
|
1165
|
+
_pendingCompactRoutes: null, // compact 期间缓存的待执行路由 Array|null
|
|
1166
|
+
_fromRole: null // 缓存路由的来源角色
|
|
1146
1167
|
};
|
|
1147
1168
|
|
|
1148
1169
|
session.roleStates.set(roleName, roleState);
|
|
@@ -1355,6 +1376,11 @@ ${allRoles.map(r => `- ${r.name}: ${roleLabel(r)} - ${r.description}`).join('\n'
|
|
|
1355
1376
|
// Role Output Processing
|
|
1356
1377
|
// =====================================================================
|
|
1357
1378
|
|
|
1379
|
+
// Context 使用率阈值常量
|
|
1380
|
+
const MAX_CONTEXT = 128000; // API max_prompt_tokens 限制
|
|
1381
|
+
const COMPACT_THRESHOLD = 0.85; // 85% → 触发预防性 compact
|
|
1382
|
+
const CLEAR_THRESHOLD = 0.95; // 95% → compact 后仍超限则 clear + rebuild
|
|
1383
|
+
|
|
1358
1384
|
/**
|
|
1359
1385
|
* 处理角色的流式输出
|
|
1360
1386
|
*/
|
|
@@ -1370,6 +1396,22 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
1370
1396
|
continue;
|
|
1371
1397
|
}
|
|
1372
1398
|
|
|
1399
|
+
// ★ compact 消息过滤(compact 期间只放行 result,其余过滤)
|
|
1400
|
+
if (roleState._compacting && message.type !== 'result') {
|
|
1401
|
+
if (message.type === 'system') {
|
|
1402
|
+
if (message.subtype === 'compact_boundary') {
|
|
1403
|
+
roleState._compactSummaryPending = true;
|
|
1404
|
+
}
|
|
1405
|
+
continue; // 过滤所有 compact 期间的 system 消息
|
|
1406
|
+
}
|
|
1407
|
+
if (message.type === 'user' && roleState._compactSummaryPending) {
|
|
1408
|
+
roleState._compactSummaryPending = false;
|
|
1409
|
+
continue; // 过滤 compact summary
|
|
1410
|
+
}
|
|
1411
|
+
// 其他消息(assistant 等)在 compact 期间也过滤
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1373
1415
|
if (message.type === 'assistant') {
|
|
1374
1416
|
// 累积文本用于路由解析
|
|
1375
1417
|
const content = message.message?.content;
|
|
@@ -1424,12 +1466,99 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
1424
1466
|
roleState.lastOutputTokens = message.usage.output_tokens || 0;
|
|
1425
1467
|
}
|
|
1426
1468
|
|
|
1469
|
+
// ★ compact turn 完成的处理
|
|
1470
|
+
if (roleState._compacting) {
|
|
1471
|
+
roleState._compacting = false;
|
|
1472
|
+
const postCompactTokens = message.usage?.input_tokens || 0;
|
|
1473
|
+
const postCompactPercentage = postCompactTokens / MAX_CONTEXT;
|
|
1474
|
+
console.log(`[Crew] ${roleName} compact completed, context now at ${Math.round(postCompactPercentage * 100)}%`);
|
|
1475
|
+
|
|
1476
|
+
sendCrewMessage({
|
|
1477
|
+
type: 'crew_role_compact',
|
|
1478
|
+
sessionId: session.id,
|
|
1479
|
+
role: roleName,
|
|
1480
|
+
contextPercentage: Math.round(postCompactPercentage * 100),
|
|
1481
|
+
status: 'completed'
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// Layer 2: compact 后仍超 95% → clear + rebuild
|
|
1485
|
+
if (postCompactPercentage >= CLEAR_THRESHOLD) {
|
|
1486
|
+
console.warn(`[Crew] ${roleName} still at ${Math.round(postCompactPercentage * 100)}% after compact, escalating to clear`);
|
|
1487
|
+
|
|
1488
|
+
await clearRoleSessionId(session.sharedDir, roleName);
|
|
1489
|
+
roleState.claudeSessionId = null;
|
|
1490
|
+
|
|
1491
|
+
if (roleState.abortController) roleState.abortController.abort();
|
|
1492
|
+
roleState.query = null;
|
|
1493
|
+
roleState.inputStream = null;
|
|
1494
|
+
|
|
1495
|
+
sendCrewMessage({
|
|
1496
|
+
type: 'crew_role_compact',
|
|
1497
|
+
sessionId: session.id,
|
|
1498
|
+
role: roleName,
|
|
1499
|
+
status: 'cleared'
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// 重新 dispatch 缓存的路由(用新会话)
|
|
1503
|
+
if (roleState._pendingCompactRoutes) {
|
|
1504
|
+
const routes = roleState._pendingCompactRoutes;
|
|
1505
|
+
const fromRole = roleState._fromRole;
|
|
1506
|
+
roleState._pendingCompactRoutes = null;
|
|
1507
|
+
roleState._fromRole = null;
|
|
1508
|
+
session.round++;
|
|
1509
|
+
const results = await Promise.allSettled(routes.map(route =>
|
|
1510
|
+
executeRoute(session, fromRole, route)
|
|
1511
|
+
));
|
|
1512
|
+
for (const r of results) {
|
|
1513
|
+
if (r.status === 'rejected') {
|
|
1514
|
+
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return; // abort 后 query 已清空,退出 processRoleOutput
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// 执行之前缓存的路由
|
|
1522
|
+
if (roleState._pendingCompactRoutes) {
|
|
1523
|
+
const routes = roleState._pendingCompactRoutes;
|
|
1524
|
+
const fromRole = roleState._fromRole;
|
|
1525
|
+
roleState._pendingCompactRoutes = null;
|
|
1526
|
+
roleState._fromRole = null;
|
|
1527
|
+
session.round++;
|
|
1528
|
+
const results = await Promise.allSettled(routes.map(route =>
|
|
1529
|
+
executeRoute(session, fromRole, route)
|
|
1530
|
+
));
|
|
1531
|
+
for (const r of results) {
|
|
1532
|
+
if (r.status === 'rejected') {
|
|
1533
|
+
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
continue; // 不要重复处理这个 compact result
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1427
1540
|
// ★ 持久化 sessionId(每次 turn 完成后保存,用于后续 resume)
|
|
1428
1541
|
if (roleState.claudeSessionId) {
|
|
1429
1542
|
saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
|
|
1430
1543
|
.catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
|
|
1431
1544
|
}
|
|
1432
1545
|
|
|
1546
|
+
// ★ context 使用率监控
|
|
1547
|
+
const inputTokens = message.usage?.input_tokens || 0;
|
|
1548
|
+
if (inputTokens > 0) {
|
|
1549
|
+
sendCrewMessage({
|
|
1550
|
+
type: 'crew_context_usage',
|
|
1551
|
+
sessionId: session.id,
|
|
1552
|
+
role: roleName,
|
|
1553
|
+
inputTokens,
|
|
1554
|
+
maxTokens: MAX_CONTEXT,
|
|
1555
|
+
percentage: Math.min(100, Math.round((inputTokens / MAX_CONTEXT) * 100))
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const contextPercentage = inputTokens / MAX_CONTEXT;
|
|
1560
|
+
const needCompact = contextPercentage >= COMPACT_THRESHOLD;
|
|
1561
|
+
|
|
1433
1562
|
// 解析路由(支持多 ROUTE 块)
|
|
1434
1563
|
const routes = parseRoutes(roleState.accumulatedText);
|
|
1435
1564
|
roleState.accumulatedText = '';
|
|
@@ -1445,7 +1574,44 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
1445
1574
|
// 发送状态更新
|
|
1446
1575
|
sendStatusUpdate(session);
|
|
1447
1576
|
|
|
1448
|
-
//
|
|
1577
|
+
// ★ 需要 compact:缓存路由,先执行 compact
|
|
1578
|
+
if (needCompact) {
|
|
1579
|
+
console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, compacting before next dispatch`);
|
|
1580
|
+
|
|
1581
|
+
roleState._pendingCompactRoutes = routes.length > 0 ? routes : null;
|
|
1582
|
+
roleState._compacting = true;
|
|
1583
|
+
roleState._compactSummaryPending = false;
|
|
1584
|
+
roleState._fromRole = roleName;
|
|
1585
|
+
|
|
1586
|
+
// task 继承
|
|
1587
|
+
const currentTask = roleState.currentTask;
|
|
1588
|
+
if (roleState._pendingCompactRoutes) {
|
|
1589
|
+
for (const route of roleState._pendingCompactRoutes) {
|
|
1590
|
+
if (!route.taskId && currentTask) {
|
|
1591
|
+
route.taskId = currentTask.taskId;
|
|
1592
|
+
route.taskTitle = currentTask.taskTitle;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// 发送 /compact
|
|
1598
|
+
roleState.inputStream.enqueue({
|
|
1599
|
+
type: 'user',
|
|
1600
|
+
message: { role: 'user', content: '/compact' }
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
sendCrewMessage({
|
|
1604
|
+
type: 'crew_role_compact',
|
|
1605
|
+
sessionId: session.id,
|
|
1606
|
+
role: roleName,
|
|
1607
|
+
contextPercentage: Math.round(contextPercentage * 100),
|
|
1608
|
+
status: 'compacting'
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
continue; // 等 compact turn 完成
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// 执行路由(无需 compact 时)
|
|
1449
1615
|
if (routes.length > 0) {
|
|
1450
1616
|
// ★ 修复1: 多 ROUTE 只增 1 轮(round++ 从 executeRoute 移到这里)
|
|
1451
1617
|
session.round++;
|
|
@@ -1495,6 +1661,9 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
1495
1661
|
roleState.inputStream = null;
|
|
1496
1662
|
roleState.turnActive = false;
|
|
1497
1663
|
roleState.accumulatedText = '';
|
|
1664
|
+
// 重置 compact 状态(防止 compact 期间出错导致后续消息被永久过滤)
|
|
1665
|
+
roleState._compacting = false;
|
|
1666
|
+
roleState._compactSummaryPending = false;
|
|
1498
1667
|
|
|
1499
1668
|
// Step 2: 错误分类
|
|
1500
1669
|
const classification = classifyRoleError(error);
|
|
@@ -1547,12 +1716,19 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
|
|
|
1547
1716
|
});
|
|
1548
1717
|
|
|
1549
1718
|
if (roleState.lastDispatchContent) {
|
|
1719
|
+
let retryContent = roleState.lastDispatchContent;
|
|
1720
|
+
|
|
1721
|
+
// ★ context 超限时精简重派内容
|
|
1722
|
+
if (classification.needContentTrim) {
|
|
1723
|
+
retryContent = trimContentForRetry(retryContent);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1550
1726
|
if (classification.skipResume) {
|
|
1551
1727
|
await clearRoleSessionId(session.sharedDir, roleName);
|
|
1552
1728
|
}
|
|
1553
1729
|
await dispatchToRole(
|
|
1554
1730
|
session, roleName,
|
|
1555
|
-
|
|
1731
|
+
retryContent,
|
|
1556
1732
|
roleState.lastDispatchFrom || 'system',
|
|
1557
1733
|
roleState.lastDispatchTaskId,
|
|
1558
1734
|
roleState.lastDispatchTaskTitle
|