@yeaft/webchat-agent 0.1.68 → 0.1.70
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 +42 -1
- package/conversation.js +28 -0
- package/crew/role-output.js +8 -61
- package/crew/routing.js +36 -2
- package/index.js +5 -1
- package/package.json +1 -1
package/claude.js
CHANGED
|
@@ -408,7 +408,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
408
408
|
|
|
409
409
|
// 计算上下文使用百分比
|
|
410
410
|
const inputTokens = message.usage?.input_tokens || 0;
|
|
411
|
-
const maxContextTokens = 128000;
|
|
411
|
+
const maxContextTokens = ctx.CONFIG?.maxContextTokens || 128000;
|
|
412
412
|
if (inputTokens > 0) {
|
|
413
413
|
ctx.sendToServer({
|
|
414
414
|
type: 'context_usage',
|
|
@@ -497,6 +497,35 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
497
497
|
|
|
498
498
|
console.log(`[RolePlay] Auto-continuing to role: ${to}`);
|
|
499
499
|
|
|
500
|
+
// ★ Pre-send compact check for RolePlay auto-continue
|
|
501
|
+
const rpAutoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
|
|
502
|
+
const rpEstimatedNewTokens = Math.ceil(prompt.length / 3);
|
|
503
|
+
const rpEstimatedTotal = inputTokens + rpEstimatedNewTokens;
|
|
504
|
+
|
|
505
|
+
if (rpEstimatedTotal > rpAutoCompactThreshold) {
|
|
506
|
+
console.log(`[RolePlay] Pre-send compact: estimated ${rpEstimatedTotal} tokens (last: ${inputTokens} + new: ~${rpEstimatedNewTokens}) exceeds threshold ${rpAutoCompactThreshold}`);
|
|
507
|
+
ctx.sendToServer({
|
|
508
|
+
type: 'compact_status',
|
|
509
|
+
conversationId,
|
|
510
|
+
status: 'compacting',
|
|
511
|
+
message: `Auto-compacting before RolePlay continue: estimated ${rpEstimatedTotal} tokens (threshold: ${rpAutoCompactThreshold})`
|
|
512
|
+
});
|
|
513
|
+
// Store pending message and compact first
|
|
514
|
+
const userMessage = {
|
|
515
|
+
type: 'user',
|
|
516
|
+
message: { role: 'user', content: prompt }
|
|
517
|
+
};
|
|
518
|
+
state._pendingUserMessage = userMessage;
|
|
519
|
+
state.turnActive = true;
|
|
520
|
+
state.turnResultReceived = false;
|
|
521
|
+
state.inputStream.enqueue({
|
|
522
|
+
type: 'user',
|
|
523
|
+
message: { role: 'user', content: '/compact' }
|
|
524
|
+
});
|
|
525
|
+
sendConversationList();
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
|
|
500
529
|
// Re-activate the turn
|
|
501
530
|
state.turnActive = true;
|
|
502
531
|
state.turnResultReceived = false;
|
|
@@ -531,6 +560,18 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
|
|
|
531
560
|
workDir: state.workDir
|
|
532
561
|
});
|
|
533
562
|
sendConversationList();
|
|
563
|
+
|
|
564
|
+
// ★ Send pending user message after compact completes
|
|
565
|
+
if (state._pendingUserMessage && state.inputStream) {
|
|
566
|
+
const pendingMsg = state._pendingUserMessage;
|
|
567
|
+
state._pendingUserMessage = null;
|
|
568
|
+
console.log(`[${conversationId}] Sending pending message after compact`);
|
|
569
|
+
state.turnActive = true;
|
|
570
|
+
state.turnResultReceived = false;
|
|
571
|
+
sendOutput(conversationId, pendingMsg);
|
|
572
|
+
state.inputStream.enqueue(pendingMsg);
|
|
573
|
+
sendConversationList();
|
|
574
|
+
}
|
|
534
575
|
continue;
|
|
535
576
|
}
|
|
536
577
|
|
package/conversation.js
CHANGED
|
@@ -515,6 +515,34 @@ export async function handleUserInput(msg) {
|
|
|
515
515
|
};
|
|
516
516
|
|
|
517
517
|
console.log(`[${conversationId}] Sending: ${prompt.substring(0, 100)}...`);
|
|
518
|
+
|
|
519
|
+
// ★ Pre-send compact check: estimate total tokens and compact before sending if needed
|
|
520
|
+
const autoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
|
|
521
|
+
const lastInputTokens = state.lastResultInputTokens || 0;
|
|
522
|
+
const estimatedNewTokens = Math.ceil(effectivePrompt.length / 3); // conservative: ~3 chars per token
|
|
523
|
+
const estimatedTotal = lastInputTokens + estimatedNewTokens;
|
|
524
|
+
|
|
525
|
+
if (estimatedTotal > autoCompactThreshold && state.inputStream) {
|
|
526
|
+
console.log(`[${conversationId}] Pre-send compact: estimated ${estimatedTotal} tokens (last: ${lastInputTokens} + new: ~${estimatedNewTokens}) exceeds threshold ${autoCompactThreshold}`);
|
|
527
|
+
ctx.sendToServer({
|
|
528
|
+
type: 'compact_status',
|
|
529
|
+
conversationId,
|
|
530
|
+
status: 'compacting',
|
|
531
|
+
message: `Auto-compacting before send: estimated ${estimatedTotal} tokens (threshold: ${autoCompactThreshold})`
|
|
532
|
+
});
|
|
533
|
+
// Send /compact first, then the user message will be sent after compact completes
|
|
534
|
+
// by storing it as a pending message
|
|
535
|
+
state._pendingUserMessage = userMessage;
|
|
536
|
+
state.turnActive = true;
|
|
537
|
+
state.turnResultReceived = false;
|
|
538
|
+
sendConversationList();
|
|
539
|
+
state.inputStream.enqueue({
|
|
540
|
+
type: 'user',
|
|
541
|
+
message: { role: 'user', content: '/compact' }
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
518
546
|
state.turnActive = true;
|
|
519
547
|
state.turnResultReceived = false; // 重置 per-turn 去重标志
|
|
520
548
|
sendConversationList(); // 在 turnActive=true 后通知 server,确保 processing 状态正确
|
package/crew/role-output.js
CHANGED
|
@@ -6,10 +6,10 @@ import { sendCrewMessage, sendCrewOutput, sendStatusUpdate, endRoleStreaming } f
|
|
|
6
6
|
import { saveRoleSessionId, clearRoleSessionId, classifyRoleError, createRoleQuery } from './role-query.js';
|
|
7
7
|
import { parseRoutes, executeRoute, dispatchToRole } from './routing.js';
|
|
8
8
|
import { parseCompletedTasks, updateFeatureIndex, appendChangelog, saveRoleWorkSummary, updateKanban } from './task-files.js';
|
|
9
|
+
import ctx from '../context.js';
|
|
9
10
|
|
|
10
|
-
// Context
|
|
11
|
-
const
|
|
12
|
-
const CLEAR_THRESHOLD = 0.85; // 85% → 直接 clear + rebuild(不再走 compact)
|
|
11
|
+
// Context 使用率常量(运行时从 ctx.CONFIG 读取)
|
|
12
|
+
const getMaxContext = () => ctx.CONFIG?.maxContextTokens || 128000;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* 处理角色的流式输出
|
|
@@ -99,14 +99,11 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
99
99
|
sessionId: session.id,
|
|
100
100
|
role: roleName,
|
|
101
101
|
inputTokens,
|
|
102
|
-
maxTokens:
|
|
103
|
-
percentage: Math.min(100, Math.round((inputTokens /
|
|
102
|
+
maxTokens: getMaxContext(),
|
|
103
|
+
percentage: Math.min(100, Math.round((inputTokens / getMaxContext()) * 100))
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const contextPercentage = inputTokens / MAX_CONTEXT;
|
|
108
|
-
const needClear = contextPercentage >= CLEAR_THRESHOLD;
|
|
109
|
-
|
|
110
107
|
// 解析路由
|
|
111
108
|
const routes = parseRoutes(roleState.accumulatedText);
|
|
112
109
|
|
|
@@ -136,8 +133,8 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
136
133
|
}
|
|
137
134
|
}
|
|
138
135
|
|
|
139
|
-
//
|
|
140
|
-
|
|
136
|
+
// 保存本 turn 文本(供 routing.js 预检时 saveRoleWorkSummary 使用)
|
|
137
|
+
roleState.lastTurnText = roleState.accumulatedText;
|
|
141
138
|
roleState.accumulatedText = '';
|
|
142
139
|
roleState.turnActive = false;
|
|
143
140
|
|
|
@@ -149,57 +146,7 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
|
|
|
149
146
|
|
|
150
147
|
sendStatusUpdate(session);
|
|
151
148
|
|
|
152
|
-
//
|
|
153
|
-
if (needClear) {
|
|
154
|
-
console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, clearing and rebuilding`);
|
|
155
|
-
|
|
156
|
-
// 保存工作摘要到 feature 文件
|
|
157
|
-
await saveRoleWorkSummary(session, roleName, turnText).catch(e =>
|
|
158
|
-
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
159
|
-
|
|
160
|
-
// Clear 角色
|
|
161
|
-
await clearRoleSessionId(session.sharedDir, roleName);
|
|
162
|
-
roleState.claudeSessionId = null;
|
|
163
|
-
|
|
164
|
-
if (roleState.abortController) roleState.abortController.abort();
|
|
165
|
-
roleState.query = null;
|
|
166
|
-
roleState.inputStream = null;
|
|
167
|
-
|
|
168
|
-
sendCrewMessage({
|
|
169
|
-
type: 'crew_role_cleared',
|
|
170
|
-
sessionId: session.id,
|
|
171
|
-
role: roleName,
|
|
172
|
-
contextPercentage: Math.round(contextPercentage * 100),
|
|
173
|
-
reason: 'context_limit'
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// 继承 task 到路由(如有)
|
|
177
|
-
const currentTask = roleState.currentTask;
|
|
178
|
-
if (routes.length > 0) {
|
|
179
|
-
for (const route of routes) {
|
|
180
|
-
if (!route.taskId && currentTask) {
|
|
181
|
-
route.taskId = currentTask.taskId;
|
|
182
|
-
route.taskTitle = currentTask.taskTitle;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 执行路由
|
|
188
|
-
if (routes.length > 0) {
|
|
189
|
-
session.round++;
|
|
190
|
-
const results = await Promise.allSettled(routes.map(route =>
|
|
191
|
-
executeRoute(session, roleName, route)
|
|
192
|
-
));
|
|
193
|
-
for (const r of results) {
|
|
194
|
-
if (r.status === 'rejected') {
|
|
195
|
-
console.warn(`[Crew] Route execution failed:`, r.reason);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return; // query 已清空,退出
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// 执行路由(无需 clear 时)
|
|
149
|
+
// 执行路由
|
|
203
150
|
if (routes.length > 0) {
|
|
204
151
|
session.round++;
|
|
205
152
|
|
package/crew/routing.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
|
|
7
|
-
import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban } from './task-files.js';
|
|
8
|
-
import { createRoleQuery } from './role-query.js';
|
|
7
|
+
import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban, saveRoleWorkSummary } from './task-files.js';
|
|
8
|
+
import { createRoleQuery, clearRoleSessionId } from './role-query.js';
|
|
9
|
+
import ctx from '../context.js';
|
|
9
10
|
|
|
10
11
|
/** Format role label */
|
|
11
12
|
function roleLabel(r) {
|
|
@@ -193,6 +194,39 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
|
|
|
193
194
|
timestamp: Date.now()
|
|
194
195
|
});
|
|
195
196
|
|
|
197
|
+
// ★ Pre-send compact check: estimate total tokens and clear+rebuild if needed
|
|
198
|
+
const autoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
|
|
199
|
+
const lastInputTokens = roleState.lastInputTokens || 0;
|
|
200
|
+
const estimatedNewTokens = Math.ceil((typeof content === 'string' ? content.length : 0) / 3);
|
|
201
|
+
const estimatedTotal = lastInputTokens + estimatedNewTokens;
|
|
202
|
+
|
|
203
|
+
if (lastInputTokens > 0 && estimatedTotal > autoCompactThreshold) {
|
|
204
|
+
console.log(`[Crew] Pre-send compact for ${roleName}: estimated ${estimatedTotal} tokens (last: ${lastInputTokens} + new: ~${estimatedNewTokens}) exceeds threshold ${autoCompactThreshold}`);
|
|
205
|
+
|
|
206
|
+
// Save work summary before clearing (use lastTurnText since accumulatedText is cleared after result)
|
|
207
|
+
await saveRoleWorkSummary(session, roleName, roleState.lastTurnText || roleState.accumulatedText || '').catch(e =>
|
|
208
|
+
console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
|
|
209
|
+
|
|
210
|
+
// Clear role session and rebuild
|
|
211
|
+
await clearRoleSessionId(session.sharedDir, roleName);
|
|
212
|
+
roleState.claudeSessionId = null;
|
|
213
|
+
|
|
214
|
+
if (roleState.abortController) roleState.abortController.abort();
|
|
215
|
+
roleState.query = null;
|
|
216
|
+
roleState.inputStream = null;
|
|
217
|
+
|
|
218
|
+
sendCrewMessage({
|
|
219
|
+
type: 'crew_role_cleared',
|
|
220
|
+
sessionId: session.id,
|
|
221
|
+
role: roleName,
|
|
222
|
+
contextPercentage: Math.round((lastInputTokens / (ctx.CONFIG?.maxContextTokens || 128000)) * 100),
|
|
223
|
+
reason: 'pre_send_compact'
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Recreate the query (fresh Claude process)
|
|
227
|
+
roleState = await createRoleQuery(session, roleName);
|
|
228
|
+
}
|
|
229
|
+
|
|
196
230
|
// 发送
|
|
197
231
|
roleState.lastDispatchContent = content;
|
|
198
232
|
roleState.lastDispatchFrom = fromSource;
|
package/index.js
CHANGED
|
@@ -74,7 +74,11 @@ const CONFIG = {
|
|
|
74
74
|
return raw.split(',').map(s => s.trim()).filter(Boolean);
|
|
75
75
|
})(),
|
|
76
76
|
// disallowedTools 会在 loadMcpServers() 中计算
|
|
77
|
-
disallowedTools: []
|
|
77
|
+
disallowedTools: [],
|
|
78
|
+
// 最大上下文 tokens(用于百分比计算的分母)
|
|
79
|
+
maxContextTokens: parseInt(process.env.MAX_CONTEXT_TOKENS || fileConfig.maxContextTokens, 10) || 128000,
|
|
80
|
+
// Auto-compact 阈值(tokens):context 超过此值时自动触发 compact
|
|
81
|
+
autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) || 110000
|
|
78
82
|
};
|
|
79
83
|
|
|
80
84
|
// 初始化共享上下文
|