harness - 内置拦截器的修改及添加
solon-ai-harness 会为 Agent 准备三个内置拦截器:
ContextCompressionInterceptor:上下文压缩HITLInterceptor:敏感工具调用的人工审批StopLoopInterceptor:重复调用检测与纠偏
1、默认行为
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.build();
当前 Harness 默认配置如下:
- 上下文压缩:
maxMessages=100、maxContextRatio=0.75D、重试次数取modelRetries(默认 3)。消息数或估算 Token 数达到阈值时触发压缩;默认组合策略依次使用KeyInfoExtractionStrategy和HierarchicalCompressionStrategy。 - 防死循环:
new StopLoopInterceptor(3, 8)。 - HITL:默认创建
HITLInterceptor,并为bash注册BashToolStrategy。hitlEnabled默认关闭;Agent 的工具配置还必须包含hitl,拦截器才会加入该 Agent 的拦截器链。
ContextCompressionInterceptor自身的无参构造器使用maxMessages=15、maxContextRatio=0.75D;Harness 的内置默认实例不是用无参构造器创建,而是读取HarnessOptions的100和0.75D。
2、替换内置拦截器
Builder 上的三个方法是替换,不是叠加:
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.compressionInterceptor(new ContextCompressionInterceptor())
.hitlInterceptor(new HITLInterceptor())
.stopLoopInterceptor(new StopLoopInterceptor(5, 10))
.build();
替换 HITLInterceptor 后,默认注册的 BashToolStrategy 和默认的 onApproved 回调也会一并被替换,需要按需重新配置。
3、上下文压缩拦截器
压缩判断发生在 onReasonStart;模型返回上下文超长错误时,onReasonRetry 可执行强制压缩。
3.1 构造与参数
// 拦截器自身的无参默认值:15、0.75D;compressionStrategy 为 null
ContextCompressionInterceptor compression =
new ContextCompressionInterceptor();
// 指定消息窗口与策略
CompressionStrategy strategy = new CompositeCompressionStrategy()
.addStrategy(new KeyInfoExtractionStrategy())
.addStrategy(new HierarchicalCompressionStrategy());
compression = new ContextCompressionInterceptor(
100, // maxMessages,下限 10
0.75D, // maxContextRatio,范围 (0, 1]
3, // maxRetries
strategy);
compression.setMessageTriggerFactor(1.5D); // 消息数触发滞后系数,>= 1.0
compression.setMinReservedMessages(5); // 显式最小保留数;<=0 恢复动态推导
compression.setPerMessageCap(8000); // 单条消息 Token 上限;0 为自动推导
compression.setDefaultContextLength(128_000L); // 模型未声明窗口时的回退值
应用到 Harness:
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.compressionInterceptor(compression)
.build();
// 运行时同时更新 Harness 配置和当前压缩拦截器
engine.setCompressionThreshold(100, 0.75D);
3.2 保护机制
META_FIRST标记的初心链消息不参与压缩。- Assistant 工具调用与对应 Tool observation 尽量作为原子关系保留。
- Token 预算和最小保留窗口共同决定实际保留条数,因此结果不一定机械地等于
maxMessages。 - 单条消息超过
perMessageCap时会执行头尾截断。 ContextSizeEvent可报告消息数、估算 Token 数以及压缩前后统计。
4、HITL:挂起、决策与恢复
4.1 启用条件
对 Harness 创建的 Agent,以下条件要同时满足:
- Agent 工具配置包含
hitl; hitlEnabled=true;HITLInterceptor为目标工具注册了HITLStrategy。
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.toolsAdd("read", "bash", "hitl")
.hitlEnabled(true)
.build();
运行时开关:
engine.setHitlEnabled(true);
engine.setHitlEnabled(false);
setHitlEnabled 会同时更新 Harness 配置与当前 HITLInterceptor 的 enabled 状态,但不会替代工具列表中的 hitl 配置。
4.2 注册策略
HITLInterceptor hitl = new HITLInterceptor()
.onTool("bash", new BashToolStrategy())
.onTool("write", new WriteToolStrategy("write"))
.onTool("edit", new WriteToolStrategy("edit"))
.onTool("webfetch", new WebToolStrategy("webfetch"))
.onSensitiveTool("dangerous_tool");
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.toolsAdd("bash", "write", "edit", "webfetch", "hitl")
.hitlInterceptor(hitl)
.hitlEnabled(true)
.build();
HITLStrategy.evaluate(trace, args) 返回 null 表示放行,返回非空字符串表示挂起并把该字符串作为审核原因。
4.3 pending 的真实事件顺序
HITL 在 Action 开始前执行批级预检。首次发现未决的敏感调用时,典型流为:
ReasonEndEvent
-> HITLPendingEvent
-> 本次不产生 ActionStartEvent / ToolCallStartEvent / ToolCallEndEvent / ActionEndEvent
-> RunEndEvent(当前实现 isAbnormal=true,Session 仍为 pending)
-> onComplete
挂起不是 Reactor onError。这里的 RunEndEvent 是本次请求的终态,不代表逻辑任务不可恢复。UI 应结合 HITLPendingEvent 或 session.isPending() 展示“等待审批”。
HITLPendingEvent.getPendingTasks() 返回 List<HITLTask>。任务的真实字段为:
| API | 含义 |
|---|---|
getCallUuid() | 工具调用实例标识;批量场景的决策主键 |
getToolName() | 工具名 |
getArgs() | 原始参数快照 |
getComment() | 拦截原因 |
if (event instanceof HITLPendingEvent) {
HITLPendingEvent pending = (HITLPendingEvent) event;
for (HITLTask task : pending.getPendingTasks()) {
System.out.println(task.getCallUuid());
System.out.println(task.getToolName());
System.out.println(task.getArgs());
System.out.println(task.getComment());
}
}
4.4 同一 Session 以空 Prompt 恢复
先为当前批次提交决策,再复用原 Session,并调用无参 prompt();不要把恢复写成一个新的用户问题。
AgentSession session = engine.getSession(sessionId);
List<HITLTask> tasks = HITL.getPendingTasks(session);
Map<String, HITLDecision> decisions = new LinkedHashMap<>();
for (HITLTask task : tasks) {
decisions.put(task.getCallUuid(), HITLDecision.approve());
}
HITL.submitAll(session, decisions);
// 空 Prompt 恢复:复用挂起前的 runId 和 lastReasonMessage,通常直接回到 Action
engine.prompt()
.session(session)
.stream()
.subscribe(this::handleEvent, this::handleError);
恢复后的典型顺序是:
RunStartEvent(复用原 runId)
-> HITLDecidedEvent+
-> ActionStartEvent
-> ToolCallStartEvent
-> ToolCallEndEvent
-> ActionEndEvent
-> [下一轮 Reason ...]
-> RunEndEvent
-> onComplete
批准会执行工具;skip 会写入 observation 但不调用工具实现。单个敏感调用被 reject 时可能直接结束,因此 HITLDecidedEvent 后不能强制期待 Action 事件。
HITLDecidedEvent 的真实 API 为:
getCallId()、getToolName()、getArgs()、getComment()、getDecision()isApproved()、isRejected()、isSkipped()
注意命名差异:待审任务使用 HITLTask.getCallUuid();决策事件使用 HITLDecidedEvent.getCallId(),后者可与工具 Start/End 事件对齐。
4.5 alwaysAllow 与构建后回调
onApproved 只在批准决策设置了 alwaysAllow=true 时调用。若回调需要访问 engine,应先完成构建,再通过 engine.getHitlInterceptor() 设置,避免 lambda 捕获尚未初始化的局部变量。
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.toolsAdd("bash", "hitl")
.hitlEnabled(true)
.build();
engine.getHitlInterceptor().onApproved((toolName, args) -> {
Object value = args == null ? null : args.get("command");
if (value instanceof String && !((String) value).trim().isEmpty()) {
engine.addPermissionRule(
PermissionRule.allow(toolName, (String) value));
} else {
engine.addPermissionRule(PermissionRule.allow(toolName));
}
});
// 只有 alwaysAllow=true 才会触发上面的回调
HITL.submit(session, task,
HITLDecision.approve(true).comment("管理员确认"));
5、防死循环拦截器
StopLoopInterceptor stopLoop = new StopLoopInterceptor(5, 10);
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.stopLoopInterceptor(stopLoop)
.build();
检测到重复模式后,拦截器会注入纠偏提示,让模型在后续 Reason 中自行收敛;它不等同于强制终止或挂起。
6、通过 HarnessExtension 添加扩展
当前接口是三参数方法:
public interface HarnessExtension {
void configure(HarnessEngine engine,
String agentName,
ReActAgent.Builder agentBuilder);
}
构建期添加:
HarnessExtension extension = new HarnessExtension() {
@Override
public void configure(HarnessEngine engine,
String agentName,
ReActAgent.Builder agentBuilder) {
agentBuilder.defaultInterceptorAdd(new ReActInterceptor() {
@Override
public void onAgentStart(ReActTrace trace) {
System.out.println("Agent started: " + agentName);
}
});
}
};
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.extensionAdd(extension)
.build();
运行时添加和移除;变更会重建主 Agent:
HarnessExtension extension = (engine, agentName, agentBuilder) ->
agentBuilder.defaultInterceptorAdd(new MyCustomInterceptor());
engine.addExtension(extension);
engine.removeExtension(extension);
defaultInterceptorAdd(...) 是向 Agent 拦截器链追加实例;它与 Builder 上替换 Harness 内置实例的 compressionInterceptor(...)、hitlInterceptor(...)、stopLoopInterceptor(...) 不同。
7、组合示例
CompressionStrategy strategy = new CompositeCompressionStrategy()
.addStrategy(new KeyInfoExtractionStrategy())
.addStrategy(new HierarchicalCompressionStrategy());
ContextCompressionInterceptor compression =
new ContextCompressionInterceptor(100, 0.75D, 3, strategy);
HITLInterceptor hitl = new HITLInterceptor()
.onTool("bash", new BashToolStrategy())
.onTool("write", new WriteToolStrategy("write"))
.onTool("edit", new WriteToolStrategy("edit"));
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
.sessionProvider(sessionProvider)
.compressionInterceptor(compression)
.hitlInterceptor(hitl)
.hitlEnabled(true)
.stopLoopInterceptor(new StopLoopInterceptor(5, 12))
.toolsAdd("read", "write", "edit", "bash", "hitl")
.build();
// 构建后再捕获 engine
engine.getHitlInterceptor().onApproved((toolName, args) -> {
Object command = args == null ? null : args.get("command");
if (command instanceof String && !((String) command).trim().isEmpty()) {
engine.addPermissionRule(
PermissionRule.allow(toolName, (String) command));
} else {
engine.addPermissionRule(PermissionRule.allow(toolName));
}
});
engine.setCompressionThreshold(100, 0.75D);
engine.setHitlEnabled(true);