Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,11 +800,15 @@ def strip_think_blocks(agent, content: str) -> str:
# a literal <tool_call> in prose is already vanishingly rare.
for _tc_name in ("tool_call", "tool_calls", "tool_result",
"function_call", "function_calls"):
# NOTE: intentionally does NOT use re.IGNORECASE — models emit
# these tags in lower case only, and case-insensitive matching
# corrupts data content (e.g. JS/HTML that contains `<TOOL_CALL>`
# literal strings). See #72797.
content = re.sub(
rf'<{_tc_name}\b[^>]*>.*?</{_tc_name}>',
'',
content,
flags=re.DOTALL | re.IGNORECASE,
flags=re.DOTALL,
)
# 1c. <function name="...">...</function> — Gemma-style standalone
# tool call. Only strip when the tag sits at a block boundary
Expand Down Expand Up @@ -840,11 +844,12 @@ def strip_think_blocks(agent, content: str) -> str:
# unterminated <function name="..."> because a truncated tail
# during streaming may still be valuable to the user; matches
# OpenClaw's intentional asymmetry.)
# NOTE: intentionally does NOT use re.IGNORECASE — same rationale
# as the block patterns above (#72797).
content = re.sub(
r'</(?:tool_call|tool_calls|tool_result|function_call|function_calls|function)>\s*',
'',
content,
flags=re.IGNORECASE,
)
return content

Expand Down
9 changes: 7 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,13 +284,17 @@ def _strip_reasoning_tags(text: str) -> str:
flags=re.IGNORECASE,
)
# Tool-call XML blocks (openclaw/openclaw#67318).
# NOTE: intentionally does NOT use re.IGNORECASE — models emit
# these tags in lower case only, and case-insensitive matching
# corrupts data content (e.g. JS/HTML that contains `<TOOL_CALL>`
# literal strings). See #72797.
for tc_tag in ("tool_call", "tool_calls", "tool_result",
"function_call", "function_calls"):
cleaned = re.sub(
rf"<{tc_tag}\b[^>]*>.*?</{tc_tag}>\s*",
"",
cleaned,
flags=re.DOTALL | re.IGNORECASE,
flags=re.DOTALL,
)
# <function name="..."> — boundary + attribute gated to avoid prose FPs.
cleaned = re.sub(
Expand All @@ -302,11 +306,12 @@ def _strip_reasoning_tags(text: str) -> str:
flags=re.DOTALL | re.IGNORECASE,
)
# Stray tool-call close tags.
# NOTE: intentionally does NOT use re.IGNORECASE — same rationale
# as the generic tool-call tag patterns above (#72797).
cleaned = re.sub(
r'</(?:tool_call|tool_calls|tool_result|function_call|function_calls|function)>\s*',
'',
cleaned,
flags=re.IGNORECASE,
)
return cleaned.strip()

Expand Down
53 changes: 53 additions & 0 deletions plugins/tool-escalator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# tool-escalator

Auto-escalate to MoA (Mixture of Agents) when consecutive tool errors are detected.

## How it works

The plugin watches three lifecycle hooks to detect, escalate, and de-escalate:

```
T1 (your daily model, unchanged)
└─ on N (default 3) consecutive tool errors →
T2 (MoA preset — user selects manually via /moa or /model)
└─ after MoA aggregation completes → auto-de-escalate logging
```

### Three hook callbacks

| Hook | What it does |
|------|-------------|
| `post_tool_call` | Checks every tool result for error indicators (`"error"`, `"failed"`, `Error:`-prefixed lines, non-zero exit codes). Increments a session-scoped consecutive-error counter on failure; resets to zero on success. At threshold (default 3), logs an escalation decision and sets a session flag. |
| `pre_llm_call` | Checks the escalation flag and injects context into the user message when escalation is active, nudging the model or user to switch to MoA. Also detects ongoing MoA calls for de-escalation tracking. |
| `post_llm_call` | Detects MoA completion (via the pre_llm_call MoA-active marker) and clears the escalation flag, logging the de-escalation. |

### Error detection

Pragmatic pattern matching on the string representation of every tool result:

- Substring match for `"error"`, `"failed"`, `"failure"`, `"exception"`, `"traceback"`, `"timeout"`
- `Error:` / `ERROR`-prefixed lines
- Non-zero exit code patterns (`exit code #` where # ≠ 0)

## Configuration

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `plugins.entries.tool-escalator.config.threshold` | int | 3 | Consecutive tool errors before escalation |

Example `config.yaml`:

```yaml
plugins:
entries:
tool-escalator:
enabled: true
config:
threshold: 5
```

## Hooks declared

- `post_tool_call`
- `pre_llm_call`
- `post_llm_call`
Loading
Loading