perf: build system prompt once per turn, skip tools on force-text (#583)

* perf: build system prompt once per turn, skip tools on force-text, fix nudge role (#565)

Three fixes to agentic loop prompt handling:

1. Build system prompt once per turn instead of every tool iteration.
   `build_system_prompt_with_tools` is now pub; callers pass the result
   via `ReasoningContext::system_prompt` to avoid rebuilding ~1,500 tokens
   per iteration.

2. Skip `## Available Tools` section when `force_text = true`. The
   dispatcher passes a no-tools prompt variant on the final iteration,
   saving ~460 tokens and removing misleading instructions.

3. Change nudge message from `Role::System` to `Role::User`. A second
   system message mid-conversation is unsupported by most providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: revert nudge role change to keep ChatMessage::system

Copilot review correctly identified that using Role::User for the nudge
breaks compact_messages_for_retry, which uses rposition for Role::User
to find the last real user message. Role::Assistant would cause
back-to-back assistant messages. Since no production issues were reported
with the original system role, revert to ChatMessage::system.

[skip-regression-check]

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: address PR review — omit tool guidance when tools empty, rename shadowed var

- Conditionalize "Call tools…" guidelines and "## Tool Call Style" section
  in the system prompt so they are only included when tools are non-empty.
  Previously the force-text (no-tools) prompt still contained misleading
  tool-calling instructions. (Copilot review comment)

- Rename `system_prompt` → `cached_prompt` in dispatcher to avoid shadowing
  the earlier workspace identity `system_prompt` variable. (Copilot review)

- Add regression tests: `test_system_prompt_with_tools_contains_tool_guidance`
  and extended assertions in `test_system_prompt_without_tools_omits_tools_section`.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: [email protected] <[email protected]>
This commit is contained in:
Henry Park
2026-03-07 09:15:00 +00:00
committed by GitHub
co-authored by Claude Opus 4.6 [email protected] <[email protected]>
parent 424a0366a9
commit 30790439ee
14 changed files with 400 additions and 321 deletions
-54
View File
@@ -236,38 +236,6 @@ impl LlmTrace {
Ok(trace)
}
/// Replace all occurrences of `old` with `new` in tool call arguments,
/// text content, and user input throughout the trace.
///
/// Used to substitute hardcoded fixture paths (e.g. `/tmp/ironclaw_test`)
/// with dynamic `tempfile::tempdir()` paths so tests don't collide.
pub fn replace_paths(&mut self, old: &str, new: &str) {
for turn in &mut self.turns {
if turn.user_input.contains(old) {
turn.user_input = turn.user_input.replace(old, new);
}
for step in &mut turn.steps {
match &mut step.response {
TraceResponse::ToolCalls { tool_calls, .. } => {
for tc in tool_calls {
replace_in_json_value(&mut tc.arguments, old, new);
}
}
TraceResponse::Text { content, .. } => {
if content.contains(old) {
*content = content.replace(old, new);
}
}
TraceResponse::UserInput { content } => {
if content.contains(old) {
*content = content.replace(old, new);
}
}
}
}
}
}
/// Return only the playable steps from the raw steps (text + tool_calls),
/// skipping `user_input` markers. Only meaningful for recorded traces that
/// were deserialized from a flat `steps` array.
@@ -280,28 +248,6 @@ impl LlmTrace {
}
}
/// Recursively replace `old` with `new` in all string values within a JSON tree.
fn replace_in_json_value(value: &mut serde_json::Value, old: &str, new: &str) {
match value {
serde_json::Value::String(s) => {
if s.contains(old) {
*s = s.replace(old, new);
}
}
serde_json::Value::Object(map) => {
for v in map.values_mut() {
replace_in_json_value(v, old, new);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
replace_in_json_value(v, old, new);
}
}
_ => {}
}
}
// ---------------------------------------------------------------------------
// TraceLlm provider
// ---------------------------------------------------------------------------