Skip to content

Add inner latency spans for agent loop executor - #5487

Merged
serrrfirat merged 2 commits into
mainfrom
codex/driver-run-inner-latency
Jul 1, 2026
Merged

serrrfirat merged 2 commits into
mainfrom
codex/driver-run-inner-latency

Conversation

@serrrfirat

@serrrfirat serrrfirat commented Jul 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • add live latency TRACE spans around canonical agent-loop executor stages
  • break down planned-driver driver_run into cancel, budget, input, prompt, checkpoint, model, capability, post-capability, stop, exit, and ack stages
  • keep fields scoped to operational IDs and iteration; no prompt/message contents are logged

Change Type

  • Observability-only runtime instrumentation
  • No intentional product behavior change

Linked Issue

  • None

Security / DB Impact

  • Security: trace fields are limited to operational IDs, scope IDs, run/turn/thread IDs, operation name, and iteration. No prompt text, tool payloads, model output, or raw errors are emitted.
  • Database: no schema changes and no new writes.

Blast Radius

  • Reborn agent-loop executor tracing only.
  • Logs appear only when live latency tracing is enabled through the existing observability/RUST_LOG path.

Rollback Plan

  • Revert this PR to remove the new executor TRACE spans and the ironclaw_observability dependency from ironclaw_agent_loop.
  • Runtime fallback is to disable ironclaw_latency=trace logging.

Review Follow-Through

  • Gemini/CodeRabbit iteration-attribution comments addressed by preserving active loop iteration for post-capability exit cases and exit-path ACK spans.
  • CodeRabbit macro drift concern addressed by centralizing the stage macro in executor::latency instead of defining it locally in canonical.rs.

Reborn Checklist

  • Uses the existing Reborn executor/driver path
  • Does not alter model/tool routing or capability execution semantics
  • Does not log user content, prompts, tool arguments, or raw model output

Validation

  • cargo fmt --check
  • cargo check -p ironclaw_agent_loop -p ironclaw_reborn
  • cargo test -p ironclaw_agent_loop --lib
  • git diff --check

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5487 July 1, 2026 07:38 Destroyed
@github-actions github-actions Bot added scope: dependencies Dependency updates size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Adds a latency-tracing helper module (started_at, operation_ok, operation_error, result) and introduces a trace_stage! macro in the executor's canonical pipeline, wrapping stage calls (cancel, budget, prompt, model, reply/capability, stop, exit, ack) with latency instrumentation. Adds TurnCompletedStep::iteration() and the ironclaw_observability dependency.

Changes

Latency instrumentation of executor pipeline

Layer / File(s) Summary
Latency helper module and dependency
crates/ironclaw_agent_loop/src/executor/latency.rs, crates/ironclaw_agent_loop/src/executor.rs, crates/ironclaw_agent_loop/Cargo.toml
Adds started_at, operation_ok, operation_error, result for emitting success/failure latency traces via ironclaw_observability; declares mod latency; and adds the new crate dependency.
TurnCompletedStep iteration accessor
crates/ironclaw_agent_loop/src/executor.rs
Adds iteration() returning state.iteration for Continue or 0 for Exit, used as trace context.
trace_stage! macro and import wiring
crates/ironclaw_agent_loop/src/executor/canonical.rs
Introduces the local trace_stage! macro and reorders the super::{...} import list.
Pre-model stages: progress, input drain, prompt, checkpoint
crates/ironclaw_agent_loop/src/executor/canonical.rs
Wraps cancel/budget checks, progress emission, input drain, prompt, checkpoint, and pre-model ack/model-processing calls with trace_stage!/latency helpers.
Reply and capability output handling
crates/ironclaw_agent_loop/src/executor/canonical.rs
Traces reply admission, assistant reply processing, capability/post-capability processing, stop.observe, and follow-up input drain.
Stop decision and exit/ack tracing
crates/ironclaw_agent_loop/src/executor/canonical.rs
Wraps stop.decide, exit.process, and pre-exit ack delivery with traced latency calls on the Stop branch.
Prompt resume and skip-model branch tracing
crates/ironclaw_agent_loop/src/executor/canonical.rs
Applies equivalent traced instrumentation to resume-path capability/stop/exit calls and the SkipModel branch's ack/exit calls.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Pipeline as DefaultExecutorPipeline
    participant TraceStage as trace_stage! macro
    participant Latency as latency module
    participant Observability as ironclaw_observability

    Pipeline->>TraceStage: invoke stage (e.g. model.process, exit.process)
    TraceStage->>Latency: started_at()
    TraceStage->>Pipeline: await stage future
    Pipeline-->>TraceStage: Result<T, E>
    TraceStage->>Latency: result(operation, context, iteration, started_at, result)
    alt Ok
        Latency->>Observability: operation_ok trace event
    else Err
        Latency->>Observability: operation_error trace event
    end
    TraceStage-->>Pipeline: return original Result
Loading

No repo invariant docs (CLAUDE.md/AGENTS.md/.claude/rules) provided in this diff for citation. Flagging purely from the diff:

  • operation_ok/operation_error early-return when started_at is None — verify trace_stage! guarantees started_at is always captured before the awaited future runs, otherwise traces silently drop for the first stage in a chain.
  • operation_error<E: ?Sized> takes _error: &E (unused) — confirm no error detail (secrets/payload) is intended to leak into trace fields later; currently it's inert but the parameter shape invites someone to wire raw error content into observability without redaction review.
  • trace_stage! is defined locally in canonical.rs rather than in latency.rs — check this isn't duplicated per-file elsewhere in the crate (macro drift risk).
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the observability-focused executor tracing changes, even though it is not Conventional Commits style.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description covers all required template sections with concrete summary, impact, rollback, validation, and review notes.

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.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request integrates the "ironclaw_observability" crate to add latency tracing across various stages of the agent loop executor. It introduces a "trace_stage!" macro to measure and log the duration of operations. The review feedback correctly identifies three locations where the iteration number is hardcoded to "0" instead of using "state.iteration", which is necessary for accurate metrics tracking.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

state.iteration,
self.exit.process(ctx, ExitInput { state, kind })
)?;
trace_stage!("ack_pending_input_before_exit", 0, ack.ack(host))?;

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.

medium

The iteration number is hardcoded to 0 here, but the actual iteration is available in state.iteration. Using state.iteration ensures that the latency metrics for this ACK operation are correctly associated with the specific iteration of the loop.

Suggested change
trace_stage!("ack_pending_input_before_exit", 0, ack.ack(host))?;
trace_stage!("ack_pending_input_before_exit", state.iteration, ack.ack(host))?;

state.iteration,
self.exit.process(ctx, ExitInput { state, kind })
)?;
trace_stage!("ack_pending_input_before_exit_resume", 0, ack.ack(host))?;

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.

medium

The iteration number is hardcoded to 0 here, but the actual iteration is available in state.iteration. Using state.iteration ensures that the latency metrics for this ACK operation are correctly associated with the specific iteration of the loop.

Suggested change
trace_stage!("ack_pending_input_before_exit_resume", 0, ack.ack(host))?;
trace_stage!("ack_pending_input_before_exit_resume", state.iteration, ack.ack(host))?;

Comment on lines +500 to +504
trace_stage!(
"ack_pending_input_before_exit_skip_model",
0,
ack.ack(host)
)?;

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.

medium

The iteration number is hardcoded to 0 here, but the actual iteration is available in state.iteration. Using state.iteration ensures that the latency metrics for this ACK operation are correctly associated with the specific iteration of the loop.

Suggested change
trace_stage!(
"ack_pending_input_before_exit_skip_model",
0,
ack.ack(host)
)?;
trace_stage!(
"ack_pending_input_before_exit_skip_model",
state.iteration,
ack.ack(host)
)?;

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_agent_loop/src/executor.rs`:
- Around line 194-201: The TurnCompletedStep::iteration helper is synthesizing a
sentinel 0 for Exit, which causes canonical.rs tracing to lose the live loop
iteration when labeling post_capability and post_capability_resume. Update
TurnCompletedStep::iteration and the callers around assistant_reply/capabilities
so Exit carries or preserves the current iteration from the caller/state instead
of returning 0, and use that value when emitting the trace boundary.

In `@crates/ironclaw_agent_loop/src/executor/canonical.rs`:
- Around line 329-334: The exit-path ack spans are being recorded with a
hardcoded iteration of 0, which makes the latency traces look like iteration-0
events instead of the active turn. Update the `trace_stage!` calls in the
`StopStep::Stop` exit path within `canonical.rs`—including the
`ack_pending_input_before_exit` variants—to use `state.iteration` (the same
iteration already available in each arm) rather than 0, so the spans are
attributed to the correct step.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cc4bd4d9-f857-4446-895d-c8659b411ce9

📥 Commits

Reviewing files that changed from the base of the PR and between 940ca7a and 7362e1d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (4)
  • crates/ironclaw_agent_loop/Cargo.toml
  • crates/ironclaw_agent_loop/src/executor.rs
  • crates/ironclaw_agent_loop/src/executor/canonical.rs
  • crates/ironclaw_agent_loop/src/executor/latency.rs

Comment on lines +194 to +201
impl TurnCompletedStep {
fn iteration(&self) -> u32 {
match self {
Self::Continue { state, .. } => state.iteration,
Self::Exit(_) => 0,
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Don't synthesize iteration 0 for TurnCompletedStep::Exit.

Line 198 forces Exit(_) to 0, but canonical.rs uses completed.iteration() to label post_capability and post_capability_resume. Any exit coming out of assistant_reply or capabilities will therefore be traced under iteration 0 instead of the live loop iteration. Carry the caller's current iteration through that trace boundary instead of encoding a sentinel here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_agent_loop/src/executor.rs` around lines 194 - 201, The
TurnCompletedStep::iteration helper is synthesizing a sentinel 0 for Exit, which
causes canonical.rs tracing to lose the live loop iteration when labeling
post_capability and post_capability_resume. Update TurnCompletedStep::iteration
and the callers around assistant_reply/capabilities so Exit carries or preserves
the current iteration from the caller/state instead of returning 0, and use that
value when emitting the trace boundary.

Comment thread crates/ironclaw_agent_loop/src/executor/canonical.rs Outdated
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5487 July 1, 2026 07:50 Destroyed
@serrrfirat
serrrfirat force-pushed the codex/driver-run-inner-latency branch from 76034f0 to 1880fd8 Compare July 1, 2026 07:51
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5487 July 1, 2026 07:51 Destroyed
@railway-app

railway-app Bot commented Jul 1, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5487 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 1, 2026 at 7:57 am

@serrrfirat
serrrfirat merged commit 91bfeb3 into main Jul 1, 2026
110 checks passed
@serrrfirat
serrrfirat deleted the codex/driver-run-inner-latency branch July 1, 2026 08:06
henrypark133 added a commit that referenced this pull request Jul 16, 2026
…ntaining "Secretary" aren't scrubbed as "secret" on replay — undoes #5902 tool-result eviction / re-fetch loop (+ 24KB/48KB caps) (#6129)

* docs: add design spec for enabling Reborn nudges on 2 profiles

Scopes allow_driver_specific_nudges to interactive_default and
scheduled_trigger via a builder method, avoiding a shared-base flip
that would leak into planned_default/subagent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: correct nudge-enable spec target from interactive_default to planned_default

Real production interactive/chat/CLI turns request no explicit run
profile (submit_user_turn passes requested_run_profile: None) and the
production resolver defaults that to planned_default, not the literal
interactive_profile() construct. Retargets the design accordingly and
simplifies the implementation (no shared-base change needed at all).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: add implementation plan for enabling planned_default/scheduled_trigger nudges

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs: extract shared test helpers in nudge-enable plan per thermo-nuclear review

Tasks 6/7 previously copy-pasted the same scripted scenario and
completion assertion; extract no_progress_script()/
assert_completed_via_nudge() once, following the file's existing
run_request/run_context_for_driver helper-extraction pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(ironclaw_turns): add RunProfileDefinition::with_driver_specific_nudges builder

* Revert "feat(ironclaw_turns): add RunProfileDefinition::with_driver_specific_nudges builder"

This reverts commit 326a16c.

* docs: agent-loop canonical.rs slop cleanup design spec

Scopes fix for two recurring anti-pattern instances (completion-nudge
branching from #6013, latency-span boilerplate from #5487) plus a new
guardrail rule file, per Slack discussion on ironclaw_agent_loop growth.

* fix(threads): raise result_read preview/chunk cap to undo #5902 benchmark regression

PR #5902 (fixing #5838's context-compaction crash) cut the model-visible
tool-result preview from 100,000 to 2,048 bytes and capped result_read
pagination at the same 2,048-byte chunk with no bulk-fetch option --
recovering a 100KB tool result now takes ~49 manual result_read calls,
which most agent policies won't do reliably. This is a likely cause of
the reported benchmark score regression.

Raises TOOL_RESULT_RECORD_READ_MAX_BYTES to 40KB (still per-call bounded,
which is the property #5838 actually needed -- unbounded accumulation in
the compacted transcript caused the crash, not single-call size). Derives
MAX_MODEL_OBSERVATION_BYTES from that constant (was an independent 4096
literal) so the whole-envelope validation cap can't silently fall behind
the preview cap and start dropping large observations to bare summaries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(tests): size large-echo fixture against raised result_read cap

TOOL_RESULT_RECORD_READ_MAX_BYTES was raised 2KiB -> 40KiB earlier in
this PR; local_dev_runtime_safe_preview_observer_receives_bounded_payload
used a hardcoded ~2.5KB fixture sized for the old cap, so it no longer
exceeded the new cap and the truncation path it exercises stopped
firing, failing the "raw tail must remain out of the model replay"
assertion. Size the fixture relative to the cap instead, matching the
fix already applied to the other 3 hardcoded-size fixtures in this PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(threads): word-boundary match for sensitive markers (undo "Secretary" scrub)

The replay validator matched SENSITIVE/PROMPT_INJECTION markers as raw
substrings, so `secret` matched the ordinary word `secretary`. Any tool
result containing "Secretary of the Treasury" (i.e. every OfficeQA read,
and PinchBench doc tasks) had its model-observation scrubbed on replay and
fell back to a stub — evicting the content from the transcript and sending
the model into a re-fetch loop (identical read/grep re-issued 16-26x,
result_read storms, 3-10x calls/cost, timeouts). This is the residual
#5902 regression the preview/observation cap bump alone did not fix.

Match markers on word boundaries instead: a marker only trips as a
standalone alphanumeric token. Delimiter-bounded markers (`bearer `,
`authorization:`) and standalone credentials (`client secret`) still match.

Verified on the two hardest-hit OfficeQA tasks (UID0072, UID0034): scrub
events 16+/task -> 0, result_read 12-16 -> 0-2, calls 45-173 -> 18-29,
score 0.0 -> 1.0, cost at/below pre-5902.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(threads): lower caps to 16KB/32KB + retention regression tests

Follow-up on the same PR:

- Preview cap 40KB -> 16KB and envelope cap derived as 2x (32KB). 40KB
  retained per result was too much context; 16KB holds a normal table read
  while staying bounded, and 32KB envelope comfortably fits a 16KB preview
  of ordinary text plus schema fields.

- With the preview lower, more results overflow onto the result_read path,
  so the retention of paged content matters more. That path was never a
  separate bug: a result_read chunk's observation flows through the SAME
  normalize/validate scrub as the first-look preview, so the word-boundary
  marker fix already keeps paged chunks intact (confirmed: at 16KB, tasks
  page result_read and still resolve 1.0). No InlineOnly change needed.

- Comprehensive retention tests so this can't regress:
  * document_content_preview_is_retained_on_replay_not_scrubbed — an 8KB
    "Secretary of the Treasury" preview stays intact through replay.
  * observation_envelope_cap_covers_the_preview_cap — structural guard that
    the envelope cap can never fall below the preview cap (the #5902 drift).
  * full_cap_preview_survives_replay — a max-size preview still fits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(threads): pin result_read chunk retention + bump preview cap to 24KB

- Add `result_read_chunk_observation_with_document_content_is_retained`:
  proves a paged result_read CHUNK observation (same shape
  result_read_observation emits) carrying "Secretary of the Treasury"
  survives replay intact, not scrubbed to a stub. Chunk observations run
  through the same normalize/scrub as first-look previews, so this pins
  that the paged-retrieval path ("get the rest") stays fixed too — the
  deterministic guarantee that paged content can't silently vanish.

- Preview cap 16KB -> 24KB (envelope derives to 48KB). 16KB was tight
  enough that overflow-heavy reads paged hard; 24KB holds a normal
  multi-table read while staying bounded. Retention is guaranteed by the
  tests regardless of the exact cap; 24KB is the tuned middle.

Deterministic coverage now spans all three retention failure modes:
first-look preview (document_content_preview_is_retained...), paged chunk
(this test), and envelope-vs-preview drift
(observation_envelope_cap_covers_the_preview_cap). ironclaw_threads 82/82.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(threads): retain result-read previews on replay

* fix(threads): satisfy replay CI guards

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Pranav Raja <pranav.raja@near.ai>

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5487 — 1880fd85 Deployed Jul 1, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: dependencies Dependency updates size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant