Skip to content

🕘 feat: Distinguish Tool Preparation From Tool Call Time - #16455

Open
lia-by-librechat[bot] wants to merge 8 commits into
devfrom
lia/tool-call-progress-ui
Open

lia-by-librechat[bot] wants to merge 8 commits into
devfrom
lia/tool-call-progress-ui

Conversation

@lia-by-librechat

@lia-by-librechat lia-by-librechat Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A long model-generated tool argument can make a tool row look like it ran for minutes, even when the tool itself was fast. The existing run-step lifetime is real but does not measure the host tool call.

Change

  • Consume SDK first-fragment and dispatch signals from ⏱️ feat: Expose Tool Preparation and Dispatch Timing agents#577. Generic agent/MCP tool cards, including ClickHouse calls, show Preparing before SDK handoff and Calling afterward. Completed cards show separate preparation and SDK-handoff-to-result intervals, including sub-second calls. Older messages retain an accurately labelled Total elapsed value.
  • Persist only compact step/call/timestamp markers for reconnect. Replay restores the active phase and seeds a fresh handler across HITL approval; no tool arguments are duplicated in replay metadata.
  • Scope timing by step and call so reused provider IDs, out-of-order completions, and first ID-less argument fragments cannot misattribute a sibling's time. The running timer uses the browser's own clock; the server stamps remain authoritative for completed intervals.

The preparation interval can include approval or other pre-dispatch waiting, not only model generation. The Tool call interval is SDK handoff-to-result wall time, not the MCP round trip or ClickHouse execution time. Specialized bash/code/file/memory cards do not yet show the split; their existing step lifetime is labelled Total elapsed.

SDK release

Agents SDK #577 is merged and published in @librechat/agents 3.9.8. This PR upgrades packages/api to ^3.9.8 and locks the published tarball. Its npm integrity matches the installed tarball; focused tests and all three changed TypeScript workspaces were checked against the published package, not a source checkout. Previous messages still show the Total elapsed fallback. No publication or deployment is part of this PR.

Verification

  • With the published 3.9.8 package: focused client SSE/reconnect/card/timer specs 286 passed; API tracker and resumable-event specs 25 passed; server callback specs 42 passed; data-provider timing specs 3 passed.
  • tsc --noEmit passed in packages/data-provider, packages/api and client with the published package. Data-provider and API builds passed. Affected ESLint, Prettier, import-order, package-lock validation and circular-dependency checks passed.
  • Local Lighthouse was attempted but could not complete: the first run hit this sandbox's MongoDB socket limit, and the retry booted the app but Chrome is unavailable. CI Lighthouse remains the verification gate.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review head 011931e281cacc126fc7645bce3e4e3ef0d7c08d: wires SDK first-argument and dispatch signals through server persistence and live SSE, shows distinct preparation/tool-call durations, and labels pre-SDK run-step lifetimes as Total elapsed. Focused API, data-provider, and client tests passed.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Updated review head 0dd8429b019f5d964890ca995c5b752facc84f80: formats the focused SSE/server regressions and removes the nested phase-label ternary. Focused client tests: 170 passed; API callback/tracker and shared timing tests passed. This comment supersedes the earlier head.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review head e9f9e9f1e5d057a28811e677ee46209e1d2a416e: SDK tool preparation/dispatch events are rendered and persisted with per-call durations; this revision fixes the streamed-call type narrowing. Client typecheck and focused SSE tests pass at this head. This supersedes earlier head comments.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Updated review head 332f702bbe72297af1194434ab095e6916131a7c: measured sub-second tool calls now appear next to long preparation times rather than disappearing behind the legacy 1-second total-duration threshold. Client typecheck and 172 focused client tests passed. This supersedes earlier head comments.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Self-review head 38badd02adcae7aa5e7e59eef0ba9cc045416b18: fixed two timing attribution errors in this revision. An ID-less first fragment now survives a later named fragment, and a shared-step sibling can no longer inherit that fragment's preparation time. API tracker tests: 7 passed; SSE tests: 109 passed; callback persistence tests: 41 passed. API and client typechecks, touched-file lint, import order and static checks passed. No inline review has arrived for this head yet.

Release blocker: this PR still locks @librechat/agents 3.9.7. The new phases cannot work in a release until agents#577 is merged, published, and LibreChat's dependency and lockfile are updated. The PR description now explicitly limits separate phases to generic tool cards and describes approval wait and fallback semantics. A maintainer may initiate the exact-head review from this handoff.

@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review

@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: 38badd02ad

ℹ️ 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".

Comment on lines +519 to +523
[StepEvents.ON_TOOL_CALLS_DISPATCHED]: {
handle: async (event, data) => {
toolTiming.dispatched(data);
await emitForJob({ event, data });
},

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 Upgrade the agents SDK that emits these timing events

A clean install of this commit still uses @librechat/agents 3.9.7 (packages/api/package.json and package-lock.json), which does not emit on_tool_calls_dispatched, observed_at, or the new completion timestamp. Consequently this handler is never invoked and the preparation/call timing feature silently falls back to the old total-elapsed display. Update both the dependency range and lockfile to a release containing the required SDK signals.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed release blocker, not resolved. SDK PR #577 is still open and the latest published agents release is 3.9.7. This PR must not merge as a delivered timing feature until the SDK change is merged/published and the LibreChat dependency and lockfile are updated. No unpublished or Git-branch SDK dependency is being introduced. Review fix: a502a22. The blocked release dependency is explicitly noted above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved by df59e10. Agents SDK #577 is merged and its changes are published as @librechat/agents 3.9.8. This PR now requires ^3.9.8 in packages/api/package.json and locks 3.9.8 in package-lock.json; the npm tarball integrity matches the lock. The production API resolves that published SDK and its on_tool_calls_dispatched event. Focused tests against 3.9.8 passed: 286 client, 25 API tracker/replay, 42 server callbacks, and 3 data-provider. Data-provider, API, and client typechecks, builds, and affected static checks passed. CI is running on this exact head.

finishedText={getFinishedText()}
subtitle={subtitle}
durationMs={runStepDurationMs}
toolPreparationDurationMs={toolPreparationDurationMs}

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 Base live phase timers on the client clock

toolDispatchedAt and toolPreparationStartedAt are server-authored SSE timestamps, but ElapsedTimer subtracts the supplied value from the browser's Date.now() (Elapsed.tsx:8-9). Whenever the browser clock differs from the server clock, the live phase timer can remain at zero or show an arbitrarily inflated duration even though the server-side interval calculation is valid. Store a client receipt timestamp for the live timer, while retaining the server timestamps for completed-duration subtraction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the new head. The live timer now starts on the browser clock when a server phase stamp arrives; a skewed-clock regression verifies it restarts at zero on dispatch. Review fix: a502a22. The blocked release dependency is explicitly noted above.

Comment on lines +1319 to +1320
} else if (stepEvent.event === StepEvents.ON_TOOL_CALLS_DISPATCHED) {
const { dispatched_at: at, toolCalls } = stepEvent.data;

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 Replay the dispatch phase after reconnect

This live-event branch is the only path that stamps toolDispatchedAt onto an in-progress client card, but GenerationJobManager.trackEvent does not retain on_tool_calls_dispatched in resumeState.replayEvents, and the backend tracker only writes durations when the step closes. If a resumable client reconnects after handoff but before the result, its restored aggregatedContent therefore loses the Calling state and phase timer until completion. Persist/replay this event or stamp the dispatch timestamp into the resumable aggregate.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the new head. The generation manager persists compact first-fragment and dispatch markers per call and replays them after the restored content snapshot. Replay manager and client SSE regressions pass. Review fix: a502a22. The blocked release dependency is explicitly noted above.

Comment thread packages/api/src/agents/toolTiming.ts Outdated
Comment on lines +74 to +76
firstByStep.delete(stepId);
firstCallByStep.delete(stepId);
callIdsByStep.delete(stepId);

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 Retain step-level starts until every sibling is consumed

When multiple dispatched calls share a stepId and complete out of order, calling take for one sibling unconditionally deletes all step-level ownership and start state. If the first call was seen only as an ID-less index-0 fragment and a different sibling closes first, the later take for the actual first call can no longer recover its preparation start, so its persisted preparation duration is omitted. Remove only the consumed call and clear these step maps after every dispatched sibling has been taken.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the new head. The step-owned tracker retains unresolved sibling timestamps until each dispatched call is consumed. A faster-sibling-first regression verifies attribution. Review fix: a502a22. The blocked release dependency is explicitly noted above.

Comment thread packages/api/src/agents/toolTiming.ts Outdated
Comment on lines +21 to +25
const firstByCall = new Map<string, number>();
const firstByStep = new Map<string, number>();
const firstCallByStep = new Map<string, string>();
const callIdsByStep = new Map<string, Set<string>>();
const dispatchedByCall = new Map<string, number>();

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 Scope timing state by step ownership

These maps key timing exclusively by provider call ID even though one tracker spans the whole response and LibreChat explicitly supports provider IDs repeating across agent and step ownership. If parallel agents emit the same ID concurrently, their starts, dispatches, and completions are merged or overwritten, and closing either call deletes the other's state, producing incorrect or missing persisted durations. Key the records by at least (stepId, callId) and pass the result step ID into completed.

AGENTS.md reference: AGENTS.md:L44-L47

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the new head. API records are scoped by step ID and client maps use step-and-call composite keys. Reused-ID regressions cover concurrent steps. Review fix: a502a22. The blocked release dependency is explicitly noted above.

}
return emitForJob({ event: UsageEvents.ON_TOKEN_USAGE, data: payload });
};
const toolTiming = createToolTimingTracker();

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 Carry preparation state across HITL resume

The timing tracker is created inside getDefaultHandlers, but an approval resume reconstructs the client through initializeClient and therefore creates a fresh handler set. The first-fragment timestamp captured before the pause is neither persisted in aggregated content nor restored into this new tracker, so after approval the dispatch and completion can produce only tool execution time; the preparation interval that is supposed to include approval waiting is permanently lost on the saved card. Persist the pre-dispatch stamps in resumable state or seed the rebuilt tracker from the paused run.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in the new head. The approval resume passes its already-loaded replay snapshot into the new handler; a rebuilt-handler test proves that pre-pause preparation survives approval. Review fix: a502a22. The blocked release dependency is explicitly noted above.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T01:02:58.217200Z df59e10 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review head a502a224a35d85e3211dd4ba36cd0ba2af6f95f4: addressed the five actionable timing defects in Codex's review (browser clock, reconnect replay, sibling ownership, duplicate IDs, and HITL resume). Focused client, generation replay, tracker, and callback tests pass; affected static checks pass. The sixth finding remains a release blocker: agents#577 must merge and publish after review, then LibreChat must update its SDK dependency and lockfile. No deployment or release has been made. This supersedes prior review-head comments.

@lia-by-librechat

Copy link
Copy Markdown
Contributor Author

Review head df59e10aa7b2ecb848a0e8d95209a6362d8060c3: this PR now consumes published @librechat/agents 3.9.8 from the npm registry. Its packages/api peer dependency and root lockfile both select 3.9.8; the installed published archive matches the lock integrity. The new tool-preparation and dispatch signals are present in that archive, so clean installs can deliver the UI feature. Focused tests on this published package passed (286 client, 25 API tracker/replay, 42 server callbacks, 3 data-provider); all three changed TypeScript workspaces typechecked; data-provider and API builds and affected static checks passed. CI is running on this exact head. Codex's prior SDK-release blocker is resolved in the review thread. This comment supersedes earlier review-head handoffs; a maintainer can request the next exact-head review from here.

@danny-avila

Copy link
Copy Markdown
Collaborator

@codex review the latest head

@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: df59e10aa7

ℹ️ 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".

Comment on lines +519 to +523
const toolTiming = createToolTimingTracker(toolTimingReplayEvents);
const handlers = {
[StepEvents.ON_TOOL_CALLS_DISPATCHED]: {
handle: async (event, data) => {
toolTiming.dispatched(data);

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 Move tool timing orchestration out of legacy CJS

This new tracker lifecycle, event-handler branch, and later persistence mutation place backend behavior directly in api/server/controllers/agents/callbacks.js. Repository guidance explicitly requires new branches and service calls under /api to be implemented in packages/api, leaving the CJS layer only to wire or re-export the TypeScript implementation; move the timing lifecycle adapter into that package and invoke it from here.

AGENTS.md reference: AGENTS.md:L77-L81

Useful? React with 👍 / 👎.

Comment on lines +521 to +524
[StepEvents.ON_TOOL_CALLS_DISPATCHED]: {
handle: async (event, data) => {
toolTiming.dispatched(data);
await emitForJob({ event, data });

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 Forward dispatch timing into child-activity streams

When this handler is instantiated with eventChildActivity for an event-task child agent, emitForJob calls publishEventChildActivity, but eventActivityPhases has no mapping for ON_TOOL_CALLS_DISPATCHED, so this newly handled dispatch event is silently omitted from the child stream. The subagent reducer therefore never gets the dispatch boundary needed to transition its tool cards to Calling or calculate split timing, despite SubagentUpdatePhase now advertising tool_calls_dispatched; add the child phase mapping and fold it into the child tool metadata.

Useful? React with 👍 / 👎.

const index = stepMap?.get(stepId)?.index;
const part = typeof index === 'number' ? contentParts[index] : undefined;
if (part?.type === ContentTypes.TOOL_CALL && part.tool_call) {
Object.assign(part.tool_call, toolTiming.take(part.tool_call.id, stepId));

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 Reconstruct split timings from durable events

When a reconnect or HITL resume lands on a replica without the live contentParts reference, RedisJobStore.getContentParts reconstructs the message from durable chunks, but its on_run_step_closed overlay restores only runStepStatus and runStepDurationMs, not the timing assigned here. The compact replay markers also contain no completion timestamp, and a completed step receives no later closure on which the client could recompute it, so completed cards from before a cross-replica resume fall back to Total elapsed and may subsequently be saved without their split timing; extend durable reconstruction or the closure payload to restore both intervals.

AGENTS.md reference: AGENTS.md:L49-L52

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
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.

2 participants