LibreChat/e2e/config
Danny Avila 520af663bc
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
🧵 feat: Background Tool Calls for Agents & Model Specs (#14197)
* 🧵 feat: Background Tool Calls for Agents & Model Specs

Opt-in, poll-based background tool execution. The model marks an eligible tool
call with `run_in_background: true`; the host executor registers a task, returns
a handle immediately (so the graph turn resolves), runs the tool as a detached
promise, and the model retrieves the result via a new `check_background_task`
poll tool. Host-side only — no `@librechat/agents` change.

- Opt-in mirrors `deferred_tools`: admin capability `run_in_background`
  (off by default) + per-tool `tool_options.run_in_background`.
- Model specs / ephemeral agents: `TModelSpec.runInBackground` /
  `TEphemeralAgent.run_in_background` synthesize per-tool options; both paths
  converge at `initializeAgent`.
- In-process task registry: scoped per user+conversation, idempotent by
  toolCallId (safe across resume/replay), capped, TTL-swept.
- Excludes direct-path / host-special / code-session tools. Subagents and push
  notifications are deferred follow-ups.

* 🩹 fix: Harden background tool calls (Codex review)

- Reliable per-agent execution gate: thread the injected `run_in_background`
  tool names from `initializeAgent` through `configurable.backgroundToolNames`
  (`toolRegistry` only reaches the executor for PTC/tool_search), fixing the
  silent no-op + unstripped-arg leak for ordinary event-driven tools.
- Enforce the per-tool opt-in at execution (`backgroundToolSet.has(name)`) so a
  non-opted-in tool can't be backgrounded via an extra arg.
- Gate the `check_background_task` interception on the run actually enabling
  background, so a user tool sharing that name still executes.
- Forward `backgroundToolsAvailable` to added-convo (multi-convo) agents.
- Exclude `web_search`/`file_search` from eligibility — their results are turned
  into user-visible attachments/citations only by the foreground toolEndCallback.

* 🩹 fix: Address Codex round 2 on background tool calls

- Idempotency scoped to run+turn: provider tool-call ids repeat across turns
  (e.g. `call_0`), so key the dedupe map by `runId::toolCallId` and sweep
  orphaned mappings — a later turn no longer collides with a retained task.
- Artifacts preserved: a backgrounded tool's artifact is processed through the
  same `toolEndCallback` as the foreground path (images/files/citations no
  longer silently dropped), best-effort/guarded.
- Forward the `run_in_background` capability to connected-agent discovery and
  subagent `processAgent` init, so a child agent's own event-driven tools work
  the same as when it runs as primary.
- Strip the injected flag on foreground calls of background-capable tools
  (the model may emit it as `false`) so strict MCP/action schemas don't reject.
- `check_background_task` list path returns metadata only (result_available /
  result_chars), never full results — prevents context overflow; the full
  result is returned only when a specific id is requested.

* 🩹 fix: Address Codex round 3 on background tool calls

- Exclude background-capable tools from eager execution (run.ts): a speculative
  eager dispatch of a `run_in_background` call could launch the detached task
  with partial/stale args, and that side effect can't be canceled.
- Reserve the `check_background_task` name: overwrite a colliding user/MCP tool
  with the host poll schema (with a warning) so the advertised schema matches
  the executor's interception instead of hijacking a mismatched tool.
- Don't inject background schemas into pure subagents (spawn-tool child graphs)
  whose tools don't reach the host interceptor; keep it for primary/added/
  connected agents. Subagent background is the durable follow-up.
- Thread `backgroundToolsAvailable` + `backgroundToolNames` through the
  OpenAI-compatible and Responses agent routes (was chat-only), so the same
  agent/model spec behaves consistently across surfaces.
- Exclude image-generation built-ins (dalle/flux/gemini_image_gen/image_gen_oai/
  image_edit_oai) — artifact-first tools whose files can't reliably attach to an
  already-saved turn when backgrounded.

* 🩹 fix: Address Codex round 4 on background tool calls

- Sanitize self-spawn subagent inputs: strip `run_in_background` + the
  `check_background_task` def from the parent AgentInputs reused for self-spawn,
  so the isolated child (direct/child-graph path) doesn't advertise a background
  schema it can't honor. The SDK resolver keeps a provided `agentInputs` even
  with `self: true`.
- Exclude `check_background_task` from PTC (`run_tools_with_code`) tool
  definitions — it's host-only and not callable from generated code.
- Parse stringified JSON args before deciding background dispatch and before
  stripping the flag, so string-delivered `run_in_background` is honored and
  never leaks to strict object-schema tools.
- Skip injection for tools that already declare their own `run_in_background`
  param (would otherwise hijack/strip it), and for non-object (string-input)
  schemas (would otherwise rewrite the input contract).

* 🩹 fix: Address Codex round 5 on background tool calls

- check_background_task now parses stringified JSON args, so providers that
  deliver args as a string can retrieve a specific task by id (not just list).
- Include agentId in the background dedupe key (`agentId::runId::toolCallId`):
  two agents in the same run emitting the same provider id (e.g. `call_0`) now
  launch independent tasks instead of colliding.
- Self-spawn sanitization also strips the background entries from the reused
  toolRegistry (not just toolDefinitions), so a child using tool_search/deferred
  loading can't rediscover the host-only run_in_background / check_background_task.

* 🩹 fix: Strip run_in_background from PTC target tool schemas (Codex round 6)

The PTC path already filtered out the host-only check_background_task poll tool
but still exposed target tool schemas with the injected `run_in_background` param
(the shared toolRegistry entries were mutated by applyBackgroundToolCalls). PTC
codegen doesn't go through the host background interceptor, so it could pass the
flag to an MCP/action tool (strict-schema rejection or silent foreground with no
poll). Sanitize the PTC toolDefs like the self-spawn path does.

* 🩹 fix: Sanitize background from explicit subagent inputs (Codex round 7)

A child agent reachable as a top-level/handoff agent is initialized WITH the
background capability, then reused as an explicit subagent via buildSubagentConfigs.
Round 4 only sanitized the self-spawn case; this now applies the same
stripBackgroundFromToolDefinitions/Registry to explicit child agentInputs when
`child.backgroundToolNames` is non-empty, so an isolated child graph doesn't
advertise a run_in_background / check_background_task contract it can't honor.

* 🩹 fix: Reap stuck/expired background tasks (Codex round 8)

- get() now sweeps before returning, so repeatedly polling a known
  background_task_id can't keep an expired completed task (and its retained
  result, up to 100k chars) alive past the one-hour completed TTL.
- sweep() now reaps `running` tasks older than a 30-min running TTL, marking
  them errored. Previously a detached call that never settled (hung network /
  lost MCP connection) held a running slot forever, exhausting the
  per-conversation cap and rejecting every later dispatch.

* 🩹 fix: Evict oldest settled tasks instead of blocking at the cap (Codex round 9)

Only the running-task cap gates dispatch now. The total-tasks cap
(MAX_TASKS_PER_BUCKET) bounds memory but no longer rejects new background calls:
when full, it evicts the oldest settled (completed/error) tasks to make room.
Previously 200 quick background calls in one conversation would block all new
dispatches for up to the completed-task TTL, since polling doesn't remove settled
tasks. Running is already capped, so room always frees.

* 📝 docs: Frame background tool calls as within-turn (Codex P1 contract)

Codex escalated the request-lifecycle findings to P1 on the grounds that the
advertised "poll later" contract can't be honored for genuinely long-running
calls (request-scoped MCP connections + the run abort signal are torn down at
turn end). Align the model-facing contract with what the same-run implementation
actually delivers: the run_in_background param, check_background_task, and the
dispatch handle now instruct the model to collect the result WITHIN THE SAME TURN
(backgrounded work isn't guaranteed to survive past the turn). This is
within-turn parallelism; cross-turn survival of long-running calls remains the
deliberate durable subagent follow-up. Copy/comment-only; no behavior change.

* ♻️ refactor: Cross-turn background tool calls, leak-free

Extend background tool calls from within-turn to cross-turn on a single
process, since the mechanism already supports it: the run's abort signal
never reaches the detached invoke (the graph forwards only configurable/
metadata to the tool-execute handler), so the floating promise keeps
running past turn completion and its result stays in the in-process
registry for a later turn to poll (get/list key only on
user::conversation + id, never the dispatch run/turn).

Guarantee no connection leak: ephemeral request-scoped MCP tools (runtime
{{LIBRECHAT_BODY_*}} placeholders) capture their request-scoped store at
creation and fall back to it, so config manipulation can't redirect them;
their connection is torn down at request end. Tag such tools in
createToolInstance and run them in the foreground instead of backgrounding
them. Pooled/app-level MCP and structured tools are unaffected and survive
cross-turn via their managed pools.

Reword the model-facing contract (run_in_background, check_background_task,
handle message, fileoverview) from within-turn to cross-turn on this server
(not across restart/replica, which stays the durable follow-up).

Tests: cross-turn poll retrieval; ephemeral MCP tool runs foreground.

* 🐛 fix: Guard ephemeral MCP tag against a null server config

createToolInstance can be reached with a null/stale capturedServerConfig
(cached availableTools + getServerConfig returns null, as several MCP unit
tests construct tools). The new unconditional requiresEphemeralUserConnection
call then dereferenced config.source and threw during tool construction
(CI: Tests api shard 2/3). Guard with the same serverConfig ? ... : false
pattern the other callers use; a missing config is not request-scoped.

* 🎨 fix: Deliver backgrounded tool artifacts on the poll turn

A slow backgrounded MCP/action tool resolves after its dispatch turn is
finalized: createToolEndCallback only appends to that turn's artifactPromises
(already awaited) and writes to a closed stream, so the artifact (file/citation/
UI resource) was silently dropped — check_background_task recorded only the
hasArtifact boolean. The cross-turn contract made this the common case.

Hold the artifact on the task and deliver it through the LIVE poll turn's
toolEndCallback the first time check_background_task collects that id (once,
then cleared to free memory), attributed to the original tool. Same-turn and
cross-turn now share this path since the model must poll to collect any result.

Tests: registry claim-once; artifact delivered on poll not dispatch, idempotent.

*  feat: Agent-builder toggle for background tool calls + cap tool descriptions

Add a per-MCP-tool "run in background" toggle in the agent builder, mirroring
the programmatic/deferred pattern: gated on the admin `run_in_background`
capability via useAgentCapabilities, read/written on tool_options[id]
.run_in_background through useMCPToolOptions (per-tool + bulk mark-all), and
rendered as a Zap toggle in MCPToolItem and McpSection with new locale keys.

Also cap the section tool/server descriptions (McpSection, ToolSection,
SkillSection) with max-h-40 overflow-y-auto so a long description scrolls
instead of overflowing the dialog, matching MCPToolItem's existing cap.

Tests: MCPToolItem renders/toggles the background button only when enabled.

* 🧪 fix: Mock new background hook functions in McpSection spec

* 🎨 fix: Restore background artifact when poll-turn delivery fails

* 🛡️ fix: Harden background tool call edges from review findings

- Error immediately (matching foreground) when a background-requested tool
  failed to load, instead of returning a success handle for a dead task
- Exclude ephemeral request-scoped MCP tools at injection time so the model
  never sees a run_in_background param the executor would silently downgrade;
  flip the execute-time tag to fail closed on a missing server config
- Source image-tool background exclusions from the shared imageGenTools set
  (adds missing stable-diffusion, an artifact-first live tool) instead of a
  hand-copied list
- Add check_background_task to the eager-execution exclusion list: artifact
  collection is a one-shot claim that must not fire from a speculative
  snapshot the SDK may discard
- Strip an imitated run_in_background arg on tools the executing agent never
  opted in (multi-agent history bleed), unless the tool's own schema declares
  the parameter
- Truncate oversized stored results with an explicit marker via the shared
  truncateMiddle (moved to utils/text) instead of a silent slice
- Document the at-most-once artifact delivery semantics honestly (the
  callback's downstream persistence is fire-and-forget, as in foreground)

* ♻️ refactor: Deduplicate background tool-call plumbing and tighten types

- Use the SDK's JsonSchemaType instead of a local duplicate; drop all
  as-unknown casts and type the poll-tool serializer explicitly
- Drop derivable BackgroundTask state (progress, hasArtifact) and the dead
  `enabled` param/return on applyBackgroundToolCalls (guarded at the call
  site), which also skips the defs pass when nothing opted in
- Fold the enable expression into synthesizeBackgroundToolOptions so the
  three load/added call sites can't drift
- Throttle the registry's all-buckets sweep and always sweep the accessed
  bucket, so a hot poll loop is no longer O(total tasks server-wide); bound
  retained artifact memory with a size cap
- Single-pass stripBackgroundFromToolDefinitions; pass metadata through to
  the poll-turn callback instead of a no-op reconstruction
- Collapse the client's copy-pasted boolean option families into a keyed
  factory (also removes the shared-object mutation in the bulk toggles) and
  the six toggle-button copies into one OptionToggle component

* 🧪 test: e2e coverage for cross-turn background tool calls

Proves the full contract through the real pipeline (mock harness): an agent
opts an MCP tool in via tool_options.run_in_background, the model dispatches
it detached and receives the synthetic handle while the tool is still running
(status=running in the rendered ack — the non-blocking guarantee without
timing assertions), the tool completes after its turn finalized, and a later
user turn recovers the task id from replayed history, polls
check_background_task, and renders the collected result.

- fake-mcp-server: slow_echo fixture tool (delayed echo)
- fake-model: E2E_BACKGROUND_DISPATCH / E2E_BACKGROUND_COLLECT markers
- e2e yaml: agents capabilities = defaults + run_in_background

* 🔧 fix: Close two background capability gaps from review

- Thread backgroundToolsAvailable through the OpenAI-compatible service
  (derived from app capabilities like codeEnvAvailable/statefulSessions),
  so agents with tool_options.run_in_background keep the feature on that
  route; fold the three capability derivations into one helper
- Index ephemeral MCP servers by normalizeServerName when excluding tools
  from background injection: tool names embed the normalized server name
  while mcpConfig keys the original, so exotic server names previously
  escaped the injection-time exclusion

* 🛂 fix: Fall back to configurable user identity for background task scoping

The in-repo routes merge req into the tool-execute configurable, but external
hosts of the exported OpenAI-compatible service inject their own loadTools and
may not — tasks would then register under an empty user id, collapsing
registry isolation to conversationId alone. Resolve the scoping id from
req.user.id, then configurable.user_id / user, and cover the isolation with a
foreign-user not_found test.

* 🧹 chore: Apply repo import sorter to PR-touched files
2026-07-13 12:51:36 -04:00
..
librechat.e2e.yaml 🧵 feat: Background Tool Calls for Agents & Model Specs (#14197) 2026-07-13 12:51:36 -04:00
librechat.real.yaml 📊 feat: Real-Time Context Window & Token Usage Tracking (#13670) 2026-06-13 19:38:28 -04:00