Skip to content

fix(ai): parse text tool-calls, fix guardrail/concurrent prompts, surface scope in prompt - #1415

Merged
ocervell merged 3 commits into
mainfrom
fix/ai-toolcalls-prompts
Sep 19, 2026
Merged

ocervell merged 3 commits into
mainfrom
fix/ai-toolcalls-prompts

Conversation

@ocervell

Copy link
Copy Markdown
Contributor

Consolidates three AI-engine improvements into one PR. Supersedes #1413 and #1414 (both closed in favor of this).

  • Parse text tool-calls. Some models emit tool calls as <tool_call><function=…><parameter=…> XML in message content instead of the API's structured tool-call field. parse_text_tool_calls() recovers those blocks, dispatches them like real tool calls, and strips the XML from the displayed content so the transcript stays clean. (was fix(ai): parse text/Hermes-style <tool_call> blocks when a model emits tool calls as content #1413)

  • Guardrail retry + concurrent-prompt fixes. (a) Prompt guidance so a model whose target was denied for being out of scope does not repeat the identical denied target — it retries the in-scope host/IP form or picks a different in-scope target. (b) _expire_stale_pending now scopes expiry to the same prompt type, so a permission prompt no longer cancels a concurrently-pending follow_up (and vice versa). (was fix(ai): guide guardrail-denied retries to in-scope host/IP and stop concurrent prompts from cancelling each other #1414)

  • Surface authorized scope in the prompt. Inject the run's in-scope (and out-of-scope, if present) targets into the system prompt via a <scope> block so the model knows the allowed scope up front — reducing guardrail-denied retries. Omitted entirely when no scope is configured.

Tests: AI prompt / utils / interactivity unit suites pass together (131 passed for the touched files; new TestScopeInPrompt asserts scope appears when provided and is absent otherwise). The 6 pre-existing test_ai_guardrails.py::TestEdgeCases redirect-parsing failures are unrelated (present on main).

🤖 Generated with Claude Code

ocervell and others added 3 commits September 19, 2026 23:48
…s tool calls as content

Some models emit tool calls as TEXT inside the message content (Hermes/XML
style, e.g. `<tool_call><function=NAME><parameter=KEY>VALUE...</tool_call>`)
instead of native structured `tool_calls`. litellm returns that text as
`content` with an empty `tool_calls`, so the agentic loop rendered the raw XML
as a user-facing message and then dead-ended (the terminal branch fires when
`tool_calls` is empty), never dispatching the model's intended tool call.

Add `parse_text_tool_calls()` in `secator/ai/utils.py`: when `tool_calls` is
empty but `content` carries one or more `<tool_call>...</tool_call>` blocks, it
recovers them into litellm-shaped calls (function name from `<function=NAME>`,
args from `<parameter=KEY>VALUE` segments; also accepts a JSON body) and strips
the consumed blocks from the displayed content. Values are coerced from JSON
when possible, else kept as text; malformed blocks are skipped, never raised.

The loop calls it right after reading `tool_calls`, so recovered calls flow
through the existing `_process_tool_calls` path exactly like native ones and the
raw XML is no longer shown. Native tool-call and plain-content paths are
unchanged. Adds unit tests covering XML/JSON blocks, multiple blocks, malformed
input, and dispatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…concurrent prompts from cancelling each other

Two independent AI-engine reliability fixes:

(a) Guardrail retry guidance. When a network action is denied because only a
different form of the host is in scope (an IP was used but the hostname is
allowed, or vice versa), the model retried the SAME denied target on a loop.
Add concise guidance to the <guardrails> prompt: never repeat a denied target;
if the denied value was an IP retry the in-scope hostname (and vice versa),
otherwise pick a different in-scope target.

(b) Concurrent-prompt cancellation. A permission/deny prompt and a multi-choice
follow-up prompt can be outstanding at the same time. `_expire_stale_pending`
(run when any new pending prompt is built) expired BOTH the `follow_up` and
`permission` pending docs indiscriminately, so building one prompt flipped the
other to `timed_out` — orphaning it so it stopped awaiting an answer. Scope the
expiry to the incoming prompt's own type: a permission prompt now only
supersedes a prior pending permission, a follow_up only a prior follow_up. The
same-type supersede (recovering a dead worker's stale prompt) is preserved.

Updates the test that codified the old cross-type behaviour and adds tests
proving the two prompt types no longer cancel each other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Inject the run's in-scope (and out-of-scope, if any) targets into the AI
system prompt so the model knows the allowed scope up front. This reduces
guardrail-denied retries where the model would otherwise guess a target
form that isn't permitted.

- `build_scope_section()` renders a `<scope>` block listing in-scope targets
  (with a hint to prefer the in-scope hostname form on retry) and out-of-scope
  targets; returns "" when no scope is configured, so the section is omitted.
- `get_system_prompt()` gains `in_scope`/`out_of_scope` params and appends the
  section; the AI task threads its resolved scope run-opts through.
- Test asserts the scope appears in the built prompt when provided and is
  absent otherwise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Repository: freelabz/secator/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ed45d136-3829-4c5e-b25f-af1d5530ec2a

📥 Commits

Reviewing files that changed from the base of the PR and between c542207 and b68529e.

📒 Files selected for processing (8)
  • secator/ai/interactivity.py
  • secator/ai/prompts.py
  • secator/ai/prompts/constraints/guardrails.txt
  • secator/ai/utils.py
  • secator/tasks/ai.py
  • tests/unit/test_ai_interactivity.py
  • tests/unit/test_ai_prompts.py
  • tests/unit/test_ai_utils.py
 _________________________
< I am the bug whisperer. >
 -------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ocervell
ocervell merged commit e7e515c into main Sep 19, 2026
8 of 9 checks passed
@ocervell
ocervell deleted the fix/ai-toolcalls-prompts branch September 19, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant