Skip to content

Commit 49bb518

Browse files
committed
Version 1.0.4 is expected to be released.
1 parent 2ad5043 commit 49bb518

27 files changed

Lines changed: 801 additions & 59 deletions

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,6 @@ _engine/
7171

7272
.qdrant-initialized
7373

74-
assets/scripts/archify/node_modules
74+
assets/scripts/archify/node_modules
75+
76+
docs/

counter.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ void main(List<String> args) {
7070
print('空行数 : ${stat.blankLines}');
7171
}
7272

73-
// dart compile exe counter.dart -o counter.exe
73+
// dart compile exe counter.dart -o counter.exe

how_to_use.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
- 新建工作目录可以与指定工作目录同名,这样在工作目录下面添加memory.json
77
- 在制作Skill的时候,提供openai格式的tool.json可以注入工具给Agent
88
- 可以在工作目录下自建.toolshell/skills文件夹进行当前工作目录技能注入
9-
- 如果某历史记录对应的工作目录被删除了,即使切换到此历史,模型上下文也没有了
9+
- 如果某历史记录对应的工作目录被删除了,即使切换到此历史,模型上下文也没有了
10+
- 安装一个全局技能时,最好在卡片上编辑对该技能的描述来增加技能查找相关性

lib/core/agent/agent_context.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ class AgentContext {
6666
final List<Map<String, dynamic>> messages;
6767
final MessagePipeline messagePipeline;
6868

69+
/// 模型的上下文窗口大小(token 数)
70+
final int contextWindow;
71+
6972
AgentContext({
7073
required this.llm,
7174
required this.pipeline,
@@ -76,6 +79,7 @@ class AgentContext {
7679
this.skillsSection = '',
7780
required this.messages,
7881
this.messagePipeline = const MessagePipeline(),
82+
this.contextWindow = 0,
7983
});
8084

8185
/// 创建 AgentLoop 实例
@@ -86,6 +90,7 @@ class AgentContext {
8690
messages: messages,
8791
messagePipeline: messagePipeline,
8892
hooks: hooks,
93+
contextWindow: contextWindow,
8994
);
9095

9196
/// 获取 Executor 实例(用于 AutonomousLoop 等外部消费)
@@ -412,6 +417,7 @@ class AgentContextBuilder {
412417
systemPromptPrefix: systemPromptPrefix,
413418
skillsSection: skillsSection,
414419
messages: existingMessages,
420+
contextWindow: modelCard.contextWindow,
415421
messagePipeline: MessagePipeline([
416422
if (effectiveStore && memoryManager != null && memoryCollection != null)
417423
ContextCompactor(

lib/core/agent/message_pipeline.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class MessagePipeline {
2626
/// 是否没有注册任何变换器(此时 process 是零开销透传)
2727
bool get isEmpty => _transformers.isEmpty;
2828

29+
/// 访问变换器列表(用于对话中压缩的重置标记等操作)
30+
List<MessageTransformer> get transformers => _transformers;
31+
2932
/// 执行变换管线
3033
///
3134
/// 对 [messages] 的深拷贝依次应用所有激活的变换器。

lib/core/agent/tool_pipeline.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,21 +143,31 @@ class ToolPipeline {
143143
return jsonEncode({
144144
'status': 'error',
145145
'code': 'INVALID_ARGS',
146-
'detail': 'calls[$i] 必须是对象',
146+
'detail': 'calls[$i] 必须是对象,但收到: ${item.runtimeType}',
147147
});
148148
}
149149
final tool = item['tool'];
150150
if (tool is! String || tool.isEmpty) {
151151
return jsonEncode({
152152
'status': 'error',
153153
'code': 'INVALID_ARGS',
154-
'detail': 'calls[$i] 缺少非空的 tool 字段',
154+
'detail': 'calls[$i] 缺少非空的 tool 字段。收到的 item: $item',
155155
});
156156
}
157157
final rawArgs = item['args'];
158158
final callArgs = rawArgs is Map
159159
? rawArgs.map((k, v) => MapEntry(k.toString(), v))
160160
: <String, dynamic>{};
161+
162+
// 警告:如果 args 字段缺失或不是 Map,会使用空对象,可能导致子工具缺少必需参数
163+
if (rawArgs == null) {
164+
print('[并行调用警告] calls[$i].tool=$tool: args 字段缺失,将使用空对象 {}');
165+
} else if (rawArgs is! Map) {
166+
print(
167+
'[并行调用警告] calls[$i].tool=$tool: args 不是对象类型(${rawArgs.runtimeType}),将使用空对象 {}',
168+
);
169+
}
170+
161171
calls.add(_ParallelCall(tool: tool, args: callArgs));
162172
}
163173

lib/core/agent/transformers/context_compactor.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ class ContextCompactor extends MessageTransformer {
4242

4343
/// 基于模型 context window 计算动态阈值的比例。
4444
/// 触发压缩 = contextWindow * ratio。
45-
/// 预留 25% 给 system prompt + 工具定义 + 保真区 + LLM 回复空间。
46-
static const double _thresholdRatio = 0.75;
45+
/// 预留 40% 给工具循环累积、system prompt + 工具定义 + 保真区 + LLM 回复空间。
46+
static const double _thresholdRatio = 0.60;
4747

4848
/// 动态计算出的实际阈值
4949
int get effectiveThreshold {

lib/core/logger/app_logger.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import '../settings/app_settings.dart';
44

55
/// 应用日志服务 — 拦截所有 print 输出并写入日志文件
66
///
7-
/// 日志存储在可配置目录下(默认 <rootDir>/logs/),按日期分文件。
7+
/// 日志存储在可配置目录下(默认 < rootDir >/logs/),按日期分文件。
88
/// 支持查看、清空操作,由日志页面调用。
99
class AppLogger {
1010
AppLogger._();

lib/core/server/api_server.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -405,8 +405,8 @@ class ApiServer {
405405
await for (final ev in events) {
406406
switch (ev) {
407407
case AgentReasoningToken():
408-
break;
409-
case AgentToken(text: final t):
408+
break;
409+
case AgentToken(text: final t):
410410
sendChunk({'content': t});
411411
case AgentToolStart(name: final n, args: final a):
412412
if (exposeTools) {
@@ -534,8 +534,8 @@ class ApiServer {
534534
await for (final ev in events) {
535535
switch (ev) {
536536
case AgentReasoningToken():
537-
break;
538-
case AgentToken(text: final t):
537+
break;
538+
case AgentToken(text: final t):
539539
buf.write(t);
540540
case AgentDone(content: final c):
541541
if (buf.isEmpty && c.isNotEmpty) buf.write(c);
@@ -622,8 +622,8 @@ class ApiServer {
622622
await for (final ev in events) {
623623
switch (ev) {
624624
case AgentReasoningToken():
625-
break;
626-
case AgentToken(text: final t):
625+
break;
626+
case AgentToken(text: final t):
627627
sendEvent('content_block_delta', {
628628
'type': 'content_block_delta',
629629
'index': 0,

lib/core/toolshell/agent_loop.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'dart:convert';
33
import '../llm/llm_client.dart';
44
import '../agent/agent_hook.dart';
55
import '../agent/message_pipeline.dart';
6+
import '../agent/transformers/context_compactor.dart';
7+
import '../isolate/compute_service.dart';
68
import '../logger/app_logger.dart';
79
import 'executor.dart';
810

@@ -150,6 +152,10 @@ class AgentLoop {
150152
/// 真正卡死的场景通常在几轮内就会重复同一模式,50只是防止真的失控。
151153
final int maxToolCallRounds;
152154

155+
/// 模型的上下文窗口大小(token 数)。用于对话中压缩的阈值计算。
156+
/// 0 表示未知,会使用保守默认值 128K。
157+
final int contextWindow;
158+
153159
AgentLoop({
154160
required this.llm,
155161
required this.executor,
@@ -158,6 +164,7 @@ class AgentLoop {
158164
this.messagePipeline = const MessagePipeline(),
159165
this.hooks = const [],
160166
this.maxToolCallRounds = 50,
167+
this.contextWindow = 0,
161168
});
162169

163170
/// 执行一轮对话 (用户输入 → 多轮 tool_call → 最终回复)
@@ -316,7 +323,96 @@ class AgentLoop {
316323
});
317324
}
318325

326+
// ✅ 对话中压缩检查:每 5 轮检查一次,防止单轮对话内上下文溢出
327+
if (round % 5 == 0) {
328+
await _checkAndCompressIfNeeded(round, messages);
329+
}
330+
319331
// 循环 → 把工具结果交给 LLM 继续处理
320332
}
321333
}
334+
335+
/// 对话中压缩检查:估算当前 token 数,超过阈值则触发压缩
336+
Future<void> _checkAndCompressIfNeeded(
337+
int round,
338+
List<Map<String, dynamic>> messages,
339+
) async {
340+
final tokens = _estimateTokens(messages);
341+
final threshold = _getEffectiveThreshold();
342+
343+
// 未超过阈值,不压缩
344+
if (tokens <= threshold) {
345+
return;
346+
}
347+
348+
AppLogger.instance.log(
349+
'[AgentLoop] 工具循环第 $round 轮触发对话中压缩: $tokens tokens > $threshold tokens (${(tokens / (contextWindow > 0 ? contextWindow : 128000) * 100).toStringAsFixed(1)}%)',
350+
);
351+
352+
final processed = await messagePipeline.process(messages);
353+
354+
// 检查是否真的发生了压缩
355+
if (processed.length < messages.length) {
356+
final beforeTokens = tokens;
357+
messages.clear();
358+
messages.addAll(processed);
359+
360+
// 重置压缩标记,允许下次再压缩
361+
for (final transformer in messagePipeline.transformers) {
362+
if (transformer is ContextCompactor) {
363+
transformer.resetRound();
364+
}
365+
}
366+
367+
final afterTokens = _estimateTokens(messages);
368+
final reduction = ((1 - afterTokens / beforeTokens) * 100)
369+
.toStringAsFixed(1);
370+
AppLogger.instance.log(
371+
'[AgentLoop] 压缩完成: ${messages.length} 条消息, $afterTokens tokens (压缩比: $reduction%)',
372+
);
373+
} else {
374+
AppLogger.instance.log('[AgentLoop] 压缩跳过: 消息数量未减少 (可能已是最小保留量)');
375+
}
376+
}
377+
378+
/// 估算消息列表的 token 数
379+
int _estimateTokens(List<Map<String, dynamic>> messages) {
380+
int total = 0;
381+
for (final msg in messages) {
382+
final content = msg['content'];
383+
if (content is String) {
384+
total += ComputeService.estimateTokens(content);
385+
} else if (content is List) {
386+
// Multimodal content parts
387+
for (final part in content) {
388+
if (part is Map && part['type'] == 'text') {
389+
total += ComputeService.estimateTokens(
390+
part['text'] as String? ?? '',
391+
);
392+
}
393+
}
394+
}
395+
// tool_calls 参数也算 token
396+
if (msg['tool_calls'] is List) {
397+
for (final tc in msg['tool_calls'] as List) {
398+
final args = tc['function']?['arguments'] as String? ?? '';
399+
total += ComputeService.estimateTokens(args);
400+
}
401+
}
402+
total += 4; // 每条消息的元数据开销
403+
}
404+
return total;
405+
}
406+
407+
/// 获取当前的压缩阈值
408+
int _getEffectiveThreshold() {
409+
for (final transformer in messagePipeline.transformers) {
410+
if (transformer is ContextCompactor) {
411+
return transformer.effectiveThreshold;
412+
}
413+
}
414+
// 默认:128K * 0.60 = 76.8K
415+
final ctx = contextWindow > 0 ? contextWindow : 128000;
416+
return (ctx * 0.60).toInt();
417+
}
322418
}

0 commit comments

Comments
 (0)