Skip to content

feat(events): carry tool outcome (duration/size/error) on ToolCompleted#18

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
senamakel:feat/tool-completed-outcome
Jul 4, 2026
Merged

feat(events): carry tool outcome (duration/size/error) on ToolCompleted#18
senamakel merged 1 commit into
tinyhumansai:mainfrom
senamakel:feat/tool-completed-outcome

Conversation

@senamakel

@senamakel senamakel commented Jul 4, 2026

Copy link
Copy Markdown
Member

Summary

AgentEvent::ToolCompleted gained started_at_ms but still lacked the outcome
an exporter needs — success/failure, duration, and result size — forcing
consumers to read a live, in-run side-channel. That makes journal-backed
(offline / replayed) exporters impossible: a journal replay can't see the run's
in-memory outcome map.

Add three additive, backward-compatible fields, populated in
finish_tool_call from the ToolResult the loop already has:

  • duration_ms — wall-clock (completion − started_at_ms)
  • output_bytes — result content length
  • errorToolResult::error verbatim (None == success)

Old journals deserialize unchanged (every field is
#[serde(default, skip_serializing_if = "Option::is_none")]); success is
derived from error.is_none(), so there is no bool-default trap.

Also enriches the crate Langfuse tool span to use them: end time =
start + duration (a real execution window rather than the journal-append
timestamp), failed calls marked level: "ERROR" with the reason, and
output_bytes in metadata.

Motivation

Unblocks a downstream consumer (OpenHuman) that wants to build progress/traces
from the durable event journal instead of a live per-run outcome map — the
event must be self-describing for that to hit parity.

Tests

  • A failing tool's ToolCompleted carries error / duration_ms /
    output_bytes.
  • The Langfuse batch renders the duration-based end time and the size metadata.

Commands run

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test (933 lib + integration, green)

Summary by CodeRabbit

  • New Features

    • Tool completion events now include elapsed time, output size, and any error message.
    • Observability exports now reflect tool failures more clearly and carry richer timing details.
  • Bug Fixes

    • Tool completion timestamps are now calculated more accurately when duration data is available.
    • Tool output metadata is now included more consistently in event data and exported traces.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 922260c4-08ae-4dad-bebb-714661f84f97

📥 Commits

Reviewing files that changed from the base of the PR and between 9c4a687 and e32f225.

📒 Files selected for processing (10)
  • src/harness/agent_loop/test.rs
  • src/harness/agent_loop/tools.rs
  • src/harness/events/test.rs
  • src/harness/events/types.rs
  • src/harness/observability/langfuse/mod.rs
  • src/harness/observability/langfuse/test.rs
  • src/harness/observability/test.rs
  • src/harness/testkit/test.rs
  • tests/e2e_orchestrator_subagents.rs
  • tests/e2e_registry_observability_contracts.rs
📝 Walkthrough

Walkthrough

This PR extends AgentEvent::ToolCompleted with three optional fields (duration_ms, output_bytes, error), populates them during tool call completion, updates the Langfuse exporter to use these fields for span timing/status/metadata, and updates existing tests/fixtures accordingly.

Changes

ToolCompleted Outcome Metadata

Layer / File(s) Summary
Event schema
src/harness/events/types.rs
AgentEvent::ToolCompleted gains optional duration_ms, output_bytes, and error fields with serde defaults for backward compatibility.
Emission and coverage
src/harness/agent_loop/tools.rs, src/harness/agent_loop/test.rs
finish_tool_call computes duration, output size, and error from the tool result and includes them in the emitted event; a new test validates a failing tool call produces these values.
Langfuse exporter consumption
src/harness/observability/langfuse/mod.rs, src/harness/observability/langfuse/test.rs
Tool span endTime is derived from started_at_ms + duration_ms with fallback, output_bytes is injected into span metadata, and level/statusMessage reflect error presence; tests updated for new expected values.
Test fixture updates
src/harness/events/test.rs, src/harness/observability/test.rs, src/harness/testkit/test.rs, tests/e2e_orchestrator_subagents.rs, tests/e2e_registry_observability_contracts.rs
Existing ToolCompleted event literals are updated to populate the new fields, typically as None.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Model
  participant AgentLoop
  participant Tool
  participant EventsSink
  participant LangfuseExporter

  Model->>AgentLoop: request tool call
  AgentLoop->>Tool: call()
  Tool-->>AgentLoop: ToolResult(content, error)
  AgentLoop->>AgentLoop: compute duration_ms, output_bytes, error
  AgentLoop->>EventsSink: emit ToolCompleted{duration_ms, output_bytes, error}
  EventsSink->>LangfuseExporter: forward event
  LangfuseExporter->>LangfuseExporter: derive endTime, metadata, level/statusMessage
Loading

Possibly related PRs

  • tinyhumansai/tinyagents#8: Both PRs modify AgentEvent::ToolCompleted and the Langfuse exporter's handling of tool completion events.
  • tinyhumansai/tinyagents#16: Overlaps on the same ToolCompleted/Langfuse timing schema, previously adding started_at_ms which this PR now builds on with duration_ms.

Poem

A tool call stumbled, "kaboom!" it cried,
Now duration and bytes ride right beside.
Langfuse spans stretch to their rightful end,
Errors marked red, no more to pretend.
Thump thump — this rabbit approves the trend! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding duration, size, and error outcome data to ToolCompleted events.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c4a687f07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// `error` mirrors `ToolResult::error` (`None` == success).
let duration_ms = crate::harness::ids::now_ms().saturating_sub(prepared.started_at_ms);
let output_bytes = result.content.len() as u64;
let error = result.error.clone();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep tool error text behind payload capture

When RunPolicy::capture.tool_io is false (the default), the PayloadCapture contract says ToolCompleted stays payload-free, but this clones ToolResult::error into the event unconditionally. Since ToolResult::error() duplicates the model-facing tool result content into this field, any failed tool that includes secrets or PII in its error will now be journaled and exported (Langfuse maps it to statusMessage) even though tool input/output capture was disabled; gate the message behind the same capture policy or record only a non-sensitive failure flag.

Useful? React with 👍 / 👎.

AgentEvent::ToolCompleted gained started_at_ms but still lacked the outcome an
exporter needs, forcing consumers to read a live side-channel: success/failure,
duration, and result size. That makes journal-backed (offline) exporters
impossible — a replayed journal can't see the run's in-memory outcome map.

Add three additive, backward-compatible fields, populated in finish_tool_call
from the ToolResult the loop already has:
- duration_ms: wall-clock (completion − started_at_ms)
- output_bytes: result content length
- error: ToolResult::error verbatim (None == success)

Old journals deserialize unchanged (all #[serde(default, skip_if none)]);
success is derived from error.is_none() so no bool-default trap.

Enriches the crate Langfuse tool span to use them: end time = start + duration
(a real window, not the append timestamp), failed calls marked level=ERROR with
the reason, and output_bytes in metadata.

Tests: a failing tool's ToolCompleted carries error/duration/output_bytes; the
Langfuse batch renders the duration-based end time and size.

Unblocks C4: with the outcome on the journalled event, a journal-backed
progress/trace projection can reach parity without the OpenHuman failure_map
side-channel (see docs/tinyagents-full-migration-plan/C4-journal-progress-parity-plan.md §2a).
@senamakel
senamakel force-pushed the feat/tool-completed-outcome branch from 9c4a687 to e32f225 Compare July 4, 2026 19:39

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e32f2258b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// journal-backed exporters render duration/size/success without the
// live run's state. Duration is wall-clock (completion minus start);
// `error` mirrors `ToolResult::error` (`None` == success).
let duration_ms = crate::harness::ids::now_ms().saturating_sub(prepared.started_at_ms);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stamp tool duration at actual completion

When a turn uses the concurrent tool path (multiple tool calls and no tool-wrap middleware), execute_tools_concurrently awaits every future with join_all before calling finish_tool_call in call order. Computing duration_ms here therefore measures until fold time, so a fast tool that finished early is reported as running until the slowest sibling completes (and later tools also include earlier after_tool hooks), which corrupts the journal/Langfuse execution window for parallel tools. Capture the completion timestamp/elapsed duration inside each tool future before join_all returns instead.

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit 146280f into tinyhumansai:main Jul 4, 2026
2 checks passed
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