---
title: "harness - 内置拦截器的修改及添加"
---


`solon-ai-harness` 会为 Agent 准备三个内置拦截器：

- `ContextCompressionInterceptor`：上下文压缩
- `HITLInterceptor`：敏感工具调用的人工审批
- `StopLoopInterceptor`：重复调用检测与纠偏

### 1、默认行为

```java
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 上的三个方法是替换，不是叠加：

```java
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 构造与参数

```java
// 拦截器自身的无参默认值：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：

```java
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，以下条件要同时满足：

1. Agent 工具配置包含 `hitl`；
2. `hitlEnabled=true`；
3. `HITLInterceptor` 为目标工具注册了 `HITLStrategy`。

```java
HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
        .sessionProvider(sessionProvider)
        .toolsAdd("read", "bash", "hitl")
        .hitlEnabled(true)
        .build();
```

运行时开关：

```java
engine.setHitlEnabled(true);
engine.setHitlEnabled(false);
```

`setHitlEnabled` 会同时更新 Harness 配置与当前 `HITLInterceptor` 的 enabled 状态，但不会替代工具列表中的 `hitl` 配置。

#### 4.2 注册策略

```java
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 开始前执行批级预检。首次发现未决的敏感调用时，典型流为：

```text
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()` | 拦截原因 |

```java
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()`；不要把恢复写成一个新的用户问题。

```java
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);
```

恢复后的典型顺序是：

```text
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 捕获尚未初始化的局部变量。

```java
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、防死循环拦截器

```java
StopLoopInterceptor stopLoop = new StopLoopInterceptor(5, 10);

HarnessEngine engine = HarnessEngine.of(workspace, harnessHome)
        .sessionProvider(sessionProvider)
        .stopLoopInterceptor(stopLoop)
        .build();
```

检测到重复模式后，拦截器会注入纠偏提示，让模型在后续 Reason 中自行收敛；它不等同于强制终止或挂起。

### 6、通过 HarnessExtension 添加扩展

当前接口是三参数方法：

```java
public interface HarnessExtension {
    void configure(HarnessEngine engine,
                   String agentName,
                   ReActAgent.Builder agentBuilder);
}
```

构建期添加：

```java
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：

```java
HarnessExtension extension = (engine, agentName, agentBuilder) ->
        agentBuilder.defaultInterceptorAdd(new MyCustomInterceptor());

engine.addExtension(extension);
engine.removeExtension(extension);
```

`defaultInterceptorAdd(...)` 是向 Agent 拦截器链追加实例；它与 Builder 上替换 Harness 内置实例的 `compressionInterceptor(...)`、`hitlInterceptor(...)`、`stopLoopInterceptor(...)` 不同。

### 7、组合示例

```java
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);
```
