Skip to content

📬 feat: Per-Call Tool Completion Emission via onResult Channel - #239

Merged
danny-avila merged 1 commit into
mainfrom
feat/per-call-tool-result-emission
Jun 11, 2026
Merged

danny-avila merged 1 commit into
mainfrom
feat/per-call-tool-result-emission

Conversation

@danny-avila

Copy link
Copy Markdown
Collaborator

Summary

I decoupled tool-completion UI latency from the slowest call in a batch. The host already executes tool batches in parallel (LibreChat's createToolExecuteHandler runs Promise.all over the calls), but the ON_TOOL_EXECUTE contract only exposed a single resolve(results[]) channel — so a 1-second tool's completion event waited for the 30-second tool next to it. This adds an optional per-call channel: the host may report each result through onResult the moment it settles, and ToolNode emits that call's completed run step immediately. It is purely an emission fast-path — resolve remains the authoritative batch outcome, the graph state still transitions once per batch (the next model turn inherently needs every ToolMessage), and handlers that never call onResult behave exactly as today. This composes with the eager-execution seals from #237/#238 rather than overlapping them: sealing moves when execution starts; onResult moves when results appear, including on the fully lazy path.

  • Added onResult?: (result: ToolExecuteResult) => void to ToolExecuteBatchRequest, documented as optional and non-authoritative.
  • Offered the callback only in no-hooks/no-HITL configurations — PostToolUse hooks can rewrite output (updatedOutput) and HITL can deny a call after execution, so those configurations keep batch-time emission; this mirrors the existing batch-sensitivity gate for eager execution.
  • Claimed ids synchronously on first report to dedupe duplicate onResult calls; ignored unknown ids; released the claim when a dispatch fails or is not delivered so the batch path re-emits.
  • Awaited in-flight early dispatches after the batch settles, before the batch loop decides which completions still need emitting — closing the double-emit race.
  • Mirrored the batch loop's output formatting exactly (truncation for success, standard error string for failures) and changed dispatchStepCompleted to report whether the event was delivered.
  • The companion LibreChat change is a near-one-liner: call data.onResult?.(result) inside the existing Promise.all mapper; safe to land before this releases since the callback is optional.

Change Type

  • New feature (non-breaking change which adds functionality)

Testing

  • New suite src/tools/__tests__/ToolNode.onResultCompletion.test.ts (6 tests): per-call emission lands before the batch resolves with no double emission; duplicate/unknown reports ignored; onResult withheld when hooks or HITL are configured; undelivered early dispatch falls back to batch emission; error results use the standard error formatting.
  • npx jest src/tools src/__tests__ src/stream.test.ts — 956 tests pass; npx tsc --noEmit, eslint, prettier, sort-imports clean.

Test Configuration:

  • Node 24 / npm ci; no provider credentials required.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

Tool batches execute in parallel on the host, but the ON_TOOL_EXECUTE
contract only had a single resolve(results[]) channel — so a fast
tool's completion event waited for the slowest call in the batch.

Add an optional onResult callback to ToolExecuteBatchRequest: the host
may report each result as it settles (before the final resolve), and
ToolNode emits that call's completed run step immediately. Purely an
emission fast-path — resolve remains the authoritative batch outcome,
graph state still transitions once per batch.

- Offered only in no-hooks/no-HITL configurations: PostToolUse hooks
  can rewrite output and HITL can deny a call after execution, so
  those keep batch-time emission
- Ids are claimed synchronously to dedupe duplicate reports; unknown
  ids are ignored; failed dispatches release the claim so the batch
  path re-emits
- Output formatting mirrors the batch loop exactly (truncation and
  error formatting), and dispatchStepCompleted now reports whether the
  event was delivered
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

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

@danny-avila
danny-avila merged commit 3710972 into main Jun 11, 2026
10 checks passed
@danny-avila danny-avila mentioned this pull request Sep 20, 2026
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