Skip to content

feat: add configurable tool-use enforcement#505

Open
jamiepine wants to merge 3 commits intomainfrom
feat/tool-use-enforcement
Open

feat: add configurable tool-use enforcement#505
jamiepine wants to merge 3 commits intomainfrom
feat/tool-use-enforcement

Conversation

@jamiepine
Copy link
Copy Markdown
Member

@jamiepine jamiepine commented Mar 29, 2026

Summary

  • add a reusable tool-use enforcement prompt fragment and append it automatically when the active model matches the configured policy
  • add tool_use_enforcement config support for defaults and per-agent overrides with auto, always, never, or custom model substring lists
  • apply the guidance across channel, branch, worker, cortex, compactor, and ingestion prompts so GPT-style models stop narrating intended tool work instead of doing it

Config

[defaults]
tool_use_enforcement = "auto"

[[agents]]
id = "main"
tool_use_enforcement = ["gemini", "deepseek"]

Testing

  • cargo test tool_use_enforcement --lib
  • just gate-pr

Note

This PR adds configurable tool-use enforcement to prevent models like GPT and Gemini from narrating tool work instead of executing it. The feature supports four modes: auto (detects based on model), always, never, or custom model substring lists. Configuration is applied at both defaults and per-agent levels, injecting enforcement guidance into all agent types (channels, branches, workers, cortex, compactor). A new reusable prompt fragment handles the injection logic. Includes test coverage for config parsing and model matching.

Written by Tembo for commit 7957a4b.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

Walkthrough

Adds a configurable ToolUseEnforcement feature: new config types and TOML schema, a prompt fragment, PromptEngine helpers, and runtime wiring to conditionally append strict tool-use guidance to system/worker prompts based on routed model selection.

Changes

Cohort / File(s) Summary
Config types & runtime
src/config/types.rs, src/config/toml_schema.rs, src/config/load.rs, src/config/runtime.rs, src/config.rs
Introduce ToolUseEnforcement enum, TOML schema fields, parsing/resolve logic, and hot-reloadable RuntimeConfig.tool_use_enforcement; add unit test for parsing/resolution.
Prompt fragment & engine
prompts/en/fragments/tool_use_enforcement.md.j2, src/prompts/text.rs, src/prompts/engine.rs
Add new template fragment and register it; add render_tool_use_enforcement() and maybe_append_tool_use_enforcement() to conditionally append guidance to prompts; include unit tests.
Agent / worker prompt integration
src/agent/...
src/agent/channel.rs, src/agent/channel_dispatch.rs, src/agent/cortex.rs, src/agent/cortex_chat.rs, src/agent/compactor.rs, src/agent/ingestion.rs
Load routing and enforcement config, resolve per-process routed model names, render base prompts, and conditionally append tool-use enforcement guidance before spawning/resuming workers and in relevant agent loops; compactor intentionally skips enforcement.
Tools / spawn path
src/tools/spawn_worker.rs
Apply routed model resolution and conditional enforcement append to detached worker prompt before spawning the worker.
API agent init
src/api/agents.rs
Initialize new tool_use_enforcement field to None when constructing internal agent config.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature being added—configurable tool-use enforcement—which is the primary change across all modified files.
Description check ✅ Passed The description is directly related to the changeset, providing a clear summary of the feature, configuration examples, testing guidance, and the problem it solves.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tool-use-enforcement

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 and usage tips.

@jamiepine jamiepine marked this pull request as ready for review March 29, 2026 07:27
) {
Ok(prompt) => prompt,
Err(error) => {
tracing::error!(%error, "failed to append tool-use enforcement to compactor prompt");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes compaction bail if the enforcement fragment render fails. That feels like a non-critical failure path — I'd fall back to the base prompt and keep going.

Suggested change
tracing::error!(%error, "failed to append tool-use enforcement to compactor prompt");
Ok(prompt) => match prompt_engine.maybe_append_tool_use_enforcement(
prompt.clone(),
tool_use_enforcement.as_ref(),
&model_name,
) {
Ok(prompt) => prompt,
Err(error) => {
tracing::error!(%error, "failed to append tool-use enforcement to compactor prompt");
prompt
}
},

) {
Ok(prompt) => prompt,
Err(error) => {
tracing::warn!(%error, "failed to append tool-use enforcement, using base cortex prompt");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor robustness: on append failure you can just fall back to the already-rendered base prompt instead of re-rendering the template (and potentially failing twice).

Suggested change
tracing::warn!(%error, "failed to append tool-use enforcement, using base cortex prompt");
Ok(prompt) => match prompt_engine.maybe_append_tool_use_enforcement(
prompt.clone(),
tool_use_enforcement.as_ref(),
&model_name,
) {
Ok(prompt) => prompt,
Err(error) => {
tracing::warn!(%error, "failed to append tool-use enforcement, using base cortex prompt");
prompt
}
},

let routing = rc.routing.load();
let model_name = routing.resolve(ProcessType::Worker, None).to_string();
let tool_use_enforcement = rc.tool_use_enforcement.load();
let worker_system_prompt = prompt_engine
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now the enforcement fragment gets appended before the skills listing, so the final lines of the worker preamble are the skills section. If the goal is “last instruction wins”, it might be more effective to append enforcement after skills.

Suggested change
let worker_system_prompt = prompt_engine
let worker_system_prompt = prompt_engine
.render_worker_prompt(
&rc.instance_dir.display().to_string(),
&rc.workspace_dir.display().to_string(),
sandbox_enabled,
sandbox_containment_active,
sandbox_read_allowlist,
sandbox_write_allowlist,
&tool_secret_names,
browser_config.persist_session,
worker_status_text,
)
.map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;
let skills = rc.skills.load();
let brave_search_key = (**rc.brave_search_key.load()).clone();
// Append skills listing to worker system prompt. Suggested skills are
// flagged so the worker knows the channel's intent, but it can read any
// skill it decides is relevant via the read_skill tool.
let system_prompt = match skills.render_worker_skills(suggested_skills, &prompt_engine) {
Ok(skills_prompt) if !skills_prompt.is_empty() => {
format!("{worker_system_prompt}\n\n{skills_prompt}")
}
Ok(_) => worker_system_prompt,
Err(error) => {
tracing::warn!(%error, "failed to render worker skills listing, spawning without skills context");
worker_system_prompt
}
};
let system_prompt = prompt_engine
.maybe_append_tool_use_enforcement(system_prompt, tool_use_enforcement.as_ref(), &model_name)
.map_err(|e| AgentError::Other(anyhow::anyhow!("{e}")))?;

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/agent/channel.rs (1)

2268-2294: ⚠️ Potential issue | 🟠 Major

Coalesced channel turns can bypass tool-use enforcement.

build_system_prompt() now appends enforcement, but coalesced turns use build_system_prompt_with_coalesce() (see Line 1433) which still returns the base prompt without maybe_append_tool_use_enforcement(...). That leaves a live bypass path whenever batching is active.

Suggested patch (apply same enforcement in coalesced prompt builder)
 async fn build_system_prompt_with_coalesce(
@@
-        prompt_engine.render_channel_prompt_with_links(
+        let routing = rc.routing.load();
+        let model_name = routing.resolve(ProcessType::Channel, None).to_string();
+        let tool_use_enforcement = rc.tool_use_enforcement.load();
+
+        let system_prompt = prompt_engine.render_channel_prompt_with_links(
             empty_to_none(identity_context),
             empty_to_none(memory_bulletin.to_string()),
             empty_to_none(skills_prompt),
             worker_capabilities,
             self.conversation_context.clone(),
             empty_to_none(status_text),
             coalesce_hint,
             available_channels,
             sandbox_enabled,
             org_context,
             adapter_prompt,
             project_context,
             self.backfill_transcript.clone(),
             empty_to_none(working_memory),
             empty_to_none(channel_activity_map),
-        )
+        )?;
+
+        prompt_engine.maybe_append_tool_use_enforcement(
+            system_prompt,
+            tool_use_enforcement.as_ref(),
+            &model_name,
+        )
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/channel.rs` around lines 2268 - 2294,
build_system_prompt_with_coalesce currently returns a system prompt without
invoking prompt_engine.maybe_append_tool_use_enforcement, allowing
coalesced/batched channel turns to bypass tool-use enforcement; update
build_system_prompt_with_coalesce (the coalesce path used for batching) to call
prompt_engine.maybe_append_tool_use_enforcement with the same arguments as
build_system_prompt (pass system_prompt, tool_use_enforcement.as_ref(),
&model_name) after rendering the coalesced prompt so tool-use enforcement is
applied consistently for both build_system_prompt() and
build_system_prompt_with_coalesce().
src/agent/channel_dispatch.rs (1)

1105-1117: ⚠️ Potential issue | 🟠 Major

Resumed builtin workers don't get tool-use enforcement.

The resume_idle_worker_into_state path (line 1106) calls render_worker_prompt directly, but unlike spawn_worker_inner and the task pickup path in cortex.rs, it doesn't chain .and_then(maybe_append_tool_use_enforcement(...)). This creates an inconsistency where:

  • Fresh workers → get enforcement guidance (required for outcome signaling)
  • Resumed workers → skip enforcement guidance

Given that workers must signal terminal outcomes via set_status(kind: "outcome"), add tool-use enforcement to the resume path to align with other worker spawn paths. If skipping enforcement is intentional (e.g., to preserve the exact original prompt), add an explicit comment explaining the design choice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/channel_dispatch.rs` around lines 1105 - 1117, The
resume_idle_worker_into_state path currently calls
prompt_engine.render_worker_prompt and returns its Result directly; update it to
mirror spawn_worker_inner and the task pickup path by chaining .and_then(|p|
maybe_append_tool_use_enforcement(p, &tool_definitions, &tool_secret_names)) (or
the correct argument order used elsewhere) so resumed workers receive the same
tool-use enforcement text; if the original omission is intentional, replace the
change with an explicit comment in resume_idle_worker_into_state explaining why
enforcement must be skipped and reference render_worker_prompt and
maybe_append_tool_use_enforcement in the comment.
🧹 Nitpick comments (3)
src/agent/compactor.rs (2)

128-144: Tool-use enforcement may be unnecessary for the compactor.

The compactor agent is built without any tools (line 247-250 shows AgentBuilder::new(model).preamble(...).default_max_turns(1).build() with no tool_server_handle). The tool-use enforcement guidance is designed to prevent models from narrating tool actions instead of executing them, but the compactor's sole job is producing a summary text.

Appending tool-use enforcement guidance to a toolless agent may confuse the model or waste tokens. Consider whether this should be skipped for ProcessType::Compactor.

💡 Alternative: Skip enforcement for compactor

If enforcement should be skipped, the simplest approach would be to not call maybe_append_tool_use_enforcement here, or ensure ToolUseEnforcement::should_inject() returns false for compactor contexts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/compactor.rs` around lines 128 - 144, The compactor prompt is being
appended with tool-use enforcement even though the Compactor agent is built
without tools; change the logic around
prompt_engine.maybe_append_tool_use_enforcement so it is skipped for
ProcessType::Compactor (or when no tools are configured) — check the
routing/model context (model_name or ProcessType::Compactor) or a "has tools"
flag before calling maybe_append_tool_use_enforcement and fall back to using
prompt_engine.render_static("compactor") directly; keep the existing error
handling and is_compacting write-unset path intact if you still call
maybe_append_tool_use_enforcement.

131-151: Consider using and_then for cleaner error chaining.

The nested match for error handling could be flattened using and_then, similar to how other files in this PR handle the pattern:

♻️ Optional refactor using and_then
-        let compactor_prompt = match prompt_engine.render_static("compactor") {
-            Ok(prompt) => match prompt_engine.maybe_append_tool_use_enforcement(
-                prompt,
-                tool_use_enforcement.as_ref(),
-                &model_name,
-            ) {
-                Ok(prompt) => prompt,
-                Err(error) => {
-                    tracing::error!(%error, "failed to append tool-use enforcement to compactor prompt");
-                    let mut flag = is_compacting.write().await;
-                    *flag = false;
-                    return;
-                }
-            },
-            Err(error) => {
-                tracing::error!(%error, "failed to render compactor prompt");
-                let mut flag = is_compacting.write().await;
-                *flag = false;
-                return;
-            }
-        };
+        let compactor_prompt = match prompt_engine
+            .render_static("compactor")
+            .and_then(|prompt| {
+                prompt_engine.maybe_append_tool_use_enforcement(
+                    prompt,
+                    tool_use_enforcement.as_ref(),
+                    &model_name,
+                )
+            })
+        {
+            Ok(prompt) => prompt,
+            Err(error) => {
+                tracing::error!(%error, "failed to render compactor prompt");
+                let mut flag = is_compacting.write().await;
+                *flag = false;
+                return;
+            }
+        };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/compactor.rs` around lines 131 - 151, Replace the nested match that
builds compactor_prompt by chaining the two fallible operations using and_then:
call prompt_engine.render_static("compactor").and_then(|prompt|
prompt_engine.maybe_append_tool_use_enforcement(prompt,
tool_use_enforcement.as_ref(), &model_name)), map or propagate the Err case to
log the error with tracing::error!(%error, "...") and then set *flag = false on
is_compacting.write().await before returning; ensure you still reference
compactor_prompt, prompt_engine.render_static,
prompt_engine.maybe_append_tool_use_enforcement, tool_use_enforcement,
model_name and is_compacting so the logic and error logging remain identical
while removing the nested match.
src/agent/cortex.rs (1)

1499-1513: Use the already-rendered prompt instead of re-rendering on enforcement failure.

When maybe_append_tool_use_enforcement fails, the code re-renders the cortex prompt instead of reusing the prompt variable that was already successfully rendered in the outer Ok(prompt) arm. This is wasteful and the second render could theoretically fail differently.

♻️ Proposed fix to reuse the already-rendered prompt
         let system_prompt = match prompt_engine.render_static("cortex") {
-            Ok(prompt) => match prompt_engine.maybe_append_tool_use_enforcement(
-                prompt,
+            Ok(base_prompt) => match prompt_engine.maybe_append_tool_use_enforcement(
+                base_prompt.clone(),
                 tool_use_enforcement.as_ref(),
                 &model_name,
             ) {
                 Ok(prompt) => prompt,
                 Err(error) => {
                     tracing::warn!(%error, "failed to append tool-use enforcement, using base cortex prompt");
-                    prompt_engine.render_static("cortex").unwrap_or_default()
+                    base_prompt
                 }
             },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/cortex.rs` around lines 1499 - 1513, The code currently re-calls
prompt_engine.render_static("cortex") inside the Err branch of
prompt_engine.maybe_append_tool_use_enforcement; instead, reuse the
already-rendered prompt from the outer Ok(prompt) arm to avoid double-rendering
and possible different failures. In the Err arm for
maybe_append_tool_use_enforcement (within the match that begins with
prompt_engine.render_static("cortex")), keep the original prompt variable (the
one bound by the outer Ok(prompt)) and return it (after logging the warning)
instead of calling render_static again; refer to
prompt_engine.maybe_append_tool_use_enforcement,
prompt_engine.render_static("cortex"), the prompt variable, and system_prompt to
locate and update the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/agent/ingestion.rs`:
- Around line 479-484: After building the ingestion_prompt with
prompt_engine.maybe_append_tool_use_enforcement and before recording the chunk
as completed, add a check for the memory_persistence_complete signal from the
model response and treat a missing signal as a chunk failure: if
memory_persistence_complete is absent, log an error with context (include
model_name, chunk id or source path), do not call the existing "mark chunk
completed" or delete the source file, and instead record the chunk as
failed/queued for retry so memory isn't lost; update the code paths that
currently always record completion after ingestion (look for
variables/operations around ingestion_prompt, memory_persistence_complete, and
the chunk completion/cleanup logic) to short-circuit on missing signal and
preserve the source for retry.

In `@src/config/types.rs`:
- Around line 245-279: The custom serde::Deserialize impl for ToolUseEnforcement
currently handles only toml::Value::String and Array, so bare TOML booleans
(toml::Value::Boolean) like tool_use_enforcement = true/false will fail; update
the deserialize function (impl Deserialize for ToolUseEnforcement) to add a
toml::Value::Boolean(b) match arm that maps true -> ToolUseEnforcement::Always
and false -> ToolUseEnforcement::Never (and keep existing String and Array
handling and error cases unchanged) so documented boolean configs are accepted.

---

Outside diff comments:
In `@src/agent/channel_dispatch.rs`:
- Around line 1105-1117: The resume_idle_worker_into_state path currently calls
prompt_engine.render_worker_prompt and returns its Result directly; update it to
mirror spawn_worker_inner and the task pickup path by chaining .and_then(|p|
maybe_append_tool_use_enforcement(p, &tool_definitions, &tool_secret_names)) (or
the correct argument order used elsewhere) so resumed workers receive the same
tool-use enforcement text; if the original omission is intentional, replace the
change with an explicit comment in resume_idle_worker_into_state explaining why
enforcement must be skipped and reference render_worker_prompt and
maybe_append_tool_use_enforcement in the comment.

In `@src/agent/channel.rs`:
- Around line 2268-2294: build_system_prompt_with_coalesce currently returns a
system prompt without invoking prompt_engine.maybe_append_tool_use_enforcement,
allowing coalesced/batched channel turns to bypass tool-use enforcement; update
build_system_prompt_with_coalesce (the coalesce path used for batching) to call
prompt_engine.maybe_append_tool_use_enforcement with the same arguments as
build_system_prompt (pass system_prompt, tool_use_enforcement.as_ref(),
&model_name) after rendering the coalesced prompt so tool-use enforcement is
applied consistently for both build_system_prompt() and
build_system_prompt_with_coalesce().

---

Nitpick comments:
In `@src/agent/compactor.rs`:
- Around line 128-144: The compactor prompt is being appended with tool-use
enforcement even though the Compactor agent is built without tools; change the
logic around prompt_engine.maybe_append_tool_use_enforcement so it is skipped
for ProcessType::Compactor (or when no tools are configured) — check the
routing/model context (model_name or ProcessType::Compactor) or a "has tools"
flag before calling maybe_append_tool_use_enforcement and fall back to using
prompt_engine.render_static("compactor") directly; keep the existing error
handling and is_compacting write-unset path intact if you still call
maybe_append_tool_use_enforcement.
- Around line 131-151: Replace the nested match that builds compactor_prompt by
chaining the two fallible operations using and_then: call
prompt_engine.render_static("compactor").and_then(|prompt|
prompt_engine.maybe_append_tool_use_enforcement(prompt,
tool_use_enforcement.as_ref(), &model_name)), map or propagate the Err case to
log the error with tracing::error!(%error, "...") and then set *flag = false on
is_compacting.write().await before returning; ensure you still reference
compactor_prompt, prompt_engine.render_static,
prompt_engine.maybe_append_tool_use_enforcement, tool_use_enforcement,
model_name and is_compacting so the logic and error logging remain identical
while removing the nested match.

In `@src/agent/cortex.rs`:
- Around line 1499-1513: The code currently re-calls
prompt_engine.render_static("cortex") inside the Err branch of
prompt_engine.maybe_append_tool_use_enforcement; instead, reuse the
already-rendered prompt from the outer Ok(prompt) arm to avoid double-rendering
and possible different failures. In the Err arm for
maybe_append_tool_use_enforcement (within the match that begins with
prompt_engine.render_static("cortex")), keep the original prompt variable (the
one bound by the outer Ok(prompt)) and return it (after logging the warning)
instead of calling render_static again; refer to
prompt_engine.maybe_append_tool_use_enforcement,
prompt_engine.render_static("cortex"), the prompt variable, and system_prompt to
locate and update the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f68f0bd-9b5d-479f-bdd7-6edd765efca2

📥 Commits

Reviewing files that changed from the base of the PR and between b7d5dd2 and 7957a4b.

📒 Files selected for processing (16)
  • prompts/en/fragments/tool_use_enforcement.md.j2
  • src/agent/channel.rs
  • src/agent/channel_dispatch.rs
  • src/agent/compactor.rs
  • src/agent/cortex.rs
  • src/agent/cortex_chat.rs
  • src/agent/ingestion.rs
  • src/api/agents.rs
  • src/config.rs
  • src/config/load.rs
  • src/config/runtime.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/prompts/engine.rs
  • src/prompts/text.rs
  • src/tools/spawn_worker.rs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/agent/cortex.rs (1)

3295-3314: ⚠️ Potential issue | 🟡 Minor

Error propagation blocks all task pickups if enforcement fragment is broken.

The .and_then() chain at lines 3307-3313 propagates any maybe_append_tool_use_enforcement failure, causing pickup_one_ready_task to return an error. Per the context snippet from src/prompts/engine.rs, if the "fragments/tool_use_enforcement" template is missing or broken, this will deterministically fail every pickup iteration. Since tool-use enforcement is optional guidance, fall back to the base worker prompt instead.

Suggested fix
     let worker_system_prompt = prompt_engine
         .render_worker_prompt(
             &deps.runtime_config.instance_dir.display().to_string(),
             &deps.runtime_config.workspace_dir.display().to_string(),
             sandbox_enabled,
             sandbox_containment_active,
             sandbox_read_allowlist,
             sandbox_write_allowlist,
             &tool_secret_names,
             browser_config.persist_session,
             worker_status_text,
         )
-        .and_then(|prompt| {
-            prompt_engine.maybe_append_tool_use_enforcement(
+        .map(|prompt| {
+            match prompt_engine.maybe_append_tool_use_enforcement(
                 prompt,
                 tool_use_enforcement.as_ref(),
                 &model_name,
-            )
+            ) {
+                Ok(augmented) => augmented,
+                Err(error) => {
+                    tracing::warn!(%error, "failed to append tool-use enforcement to worker prompt");
+                    prompt
+                }
+            }
         })
         .map_err(|error| anyhow::anyhow!("failed to render worker prompt: {error}"))?;

Note: This requires changing to prompt.clone() passed to maybe_append_tool_use_enforcement to retain ownership for the fallback:

         .map(|prompt| {
             match prompt_engine.maybe_append_tool_use_enforcement(
-                prompt,
+                prompt.clone(),
                 tool_use_enforcement.as_ref(),
                 &model_name,
             ) {
                 Ok(augmented) => augmented,
                 Err(error) => {
                     tracing::warn!(%error, "failed to append tool-use enforcement to worker prompt");
                     prompt
                 }
             }
         })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/cortex.rs` around lines 3295 - 3314, The current chain uses
prompt_engine.render_worker_prompt(...).and_then(|prompt|
prompt_engine.maybe_append_tool_use_enforcement(prompt, ...)) which propagates
any enforcement-template error and blocks pickup; instead capture the base
prompt from render_worker_prompt, call maybe_append_tool_use_enforcement with a
cloned prompt (e.g., prompt.clone()) and on Err return Ok(prompt) so the
worker_system_prompt falls back to the original prompt when enforcement
rendering fails, ensuring pickup_one_ready_task continues; update the code
around worker_system_prompt, using match/map_or_else or explicit match on
prompt_engine.maybe_append_tool_use_enforcement to log the enforcement error but
return the base prompt on failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/agent/cortex.rs`:
- Around line 3295-3314: The current chain uses
prompt_engine.render_worker_prompt(...).and_then(|prompt|
prompt_engine.maybe_append_tool_use_enforcement(prompt, ...)) which propagates
any enforcement-template error and blocks pickup; instead capture the base
prompt from render_worker_prompt, call maybe_append_tool_use_enforcement with a
cloned prompt (e.g., prompt.clone()) and on Err return Ok(prompt) so the
worker_system_prompt falls back to the original prompt when enforcement
rendering fails, ensuring pickup_one_ready_task continues; update the code
around worker_system_prompt, using match/map_or_else or explicit match on
prompt_engine.maybe_append_tool_use_enforcement to log the enforcement error but
return the base prompt on failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf4f0092-bd29-44c6-99cf-6aa9215eb9cb

📥 Commits

Reviewing files that changed from the base of the PR and between 7957a4b and 085fb1c.

📒 Files selected for processing (11)
  • src/agent/channel.rs
  • src/agent/channel_dispatch.rs
  • src/agent/compactor.rs
  • src/agent/cortex.rs
  • src/api/agents.rs
  • src/config.rs
  • src/config/load.rs
  • src/config/runtime.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/tools/spawn_worker.rs
✅ Files skipped from review due to trivial changes (5)
  • src/config/toml_schema.rs
  • src/config/load.rs
  • src/config.rs
  • src/tools/spawn_worker.rs
  • src/config/types.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/config/runtime.rs
  • src/api/agents.rs
  • src/agent/channel_dispatch.rs
  • src/agent/channel.rs

- Skip enforcement for toolless compactor agent
- Reuse rendered prompt on enforcement failure in cortex (avoid double render)
- Move enforcement after skills in worker spawn (last instruction wins)
- Add enforcement to resumed builtin workers and coalesced channel turns
- Return error on missing memory_persistence_complete signal in ingestion
- Accept bare TOML booleans for tool_use_enforcement config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/agent/ingestion.rs (1)

533-535: Please add a regression test for this fail-closed branch.

This guard is now what keeps a chunk retryable when the model never emits memory_persistence_complete, but the test module does not exercise that path yet. A focused test here would make future regressions much easier to catch. Based on learnings, "For changes in async/stateful paths (worker lifecycle, cancellation, retrigger, recall cache behavior), include explicit race/terminal-state reasoning in the PR summary and run targeted tests in addition to just gate-pr."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/agent/ingestion.rs` around lines 533 - 535, Add a regression test in the
ingestion.rs test module that exercises the fail-closed branch where the model
never emits the memory_persistence_complete signal: create a test (e.g.,
test_chunk_retry_when_no_memory_persistence_complete) that drives the chunk
ingestion path to completion without emitting memory_persistence_complete,
assert the function returns the Err containing the "ingestion chunk
{chunk_number}/{total_chunks} for {filename} completed without
memory_persistence_complete signal" message, and additionally assert the chunk
is marked retryable by verifying the same retry/state indicator your code uses
(e.g., the retry queue entry, chunk status field, or the function that schedules
retries). Ensure the test controls the async/model behavior deterministically
(mock/simulate no memory_persistence_complete) so it consistently hits the
branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/config/types.rs`:
- Around line 266-278: The array branch that constructs Self::Custom currently
accepts empty or whitespace-only strings (e.g., tool_use_enforcement = [""]),
causing should_inject() to match always; update the mapping in the
toml::Value::Array arm so after converting each value to a String you call
.trim() and reject entries where trimmed.is_empty() by returning
D::Error::invalid_value (with a clear message like "empty or whitespace-only
string"); this ensures the patterns Vec<String> only contains non-empty trimmed
patterns before returning Ok(Self::Custom(patterns)).

---

Nitpick comments:
In `@src/agent/ingestion.rs`:
- Around line 533-535: Add a regression test in the ingestion.rs test module
that exercises the fail-closed branch where the model never emits the
memory_persistence_complete signal: create a test (e.g.,
test_chunk_retry_when_no_memory_persistence_complete) that drives the chunk
ingestion path to completion without emitting memory_persistence_complete,
assert the function returns the Err containing the "ingestion chunk
{chunk_number}/{total_chunks} for {filename} completed without
memory_persistence_complete signal" message, and additionally assert the chunk
is marked retryable by verifying the same retry/state indicator your code uses
(e.g., the retry queue entry, chunk status field, or the function that schedules
retries). Ensure the test controls the async/model behavior deterministically
(mock/simulate no memory_persistence_complete) so it consistently hits the
branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3a825948-a232-4966-baa1-52d07bb57fc9

📥 Commits

Reviewing files that changed from the base of the PR and between 085fb1c and 3fb01be.

📒 Files selected for processing (6)
  • src/agent/channel.rs
  • src/agent/channel_dispatch.rs
  • src/agent/compactor.rs
  • src/agent/cortex.rs
  • src/agent/ingestion.rs
  • src/config/types.rs
✅ Files skipped from review due to trivial changes (2)
  • src/agent/compactor.rs
  • src/agent/channel.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/agent/channel_dispatch.rs

Comment on lines +266 to +278
toml::Value::Array(arr) => {
let patterns: Vec<String> = arr
.into_iter()
.map(|v| {
v.as_str().map(String::from).ok_or_else(|| {
D::Error::invalid_value(
serde::de::Unexpected::Other("non-string array element"),
&"array of strings",
)
})
})
.collect::<std::result::Result<_, _>>()?;
Ok(Self::Custom(patterns))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject blank custom match patterns.

tool_use_enforcement = [""] currently deserializes successfully, and then should_inject() turns into an unconditional match because every string contains the empty substring. Trim and reject empty entries here so malformed config does not silently behave like Always.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/types.rs` around lines 266 - 278, The array branch that constructs
Self::Custom currently accepts empty or whitespace-only strings (e.g.,
tool_use_enforcement = [""]), causing should_inject() to match always; update
the mapping in the toml::Value::Array arm so after converting each value to a
String you call .trim() and reject entries where trimmed.is_empty() by returning
D::Error::invalid_value (with a clear message like "empty or whitespace-only
string"); this ensures the patterns Vec<String> only contains non-empty trimmed
patterns before returning Ok(Self::Custom(patterns)).

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