🕊️ feat: Yield to Subagent Completion Wakeups (#15066)

* 🕊️ feat: yield to subagent completion wakeups

*  fix: bound wakeup status guidance
This commit is contained in:
Danny Avila 2026-08-21 01:45:42 -04:00 committed by GitHub
parent 061e4b02a3
commit a5cb041f47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 390 additions and 26 deletions

View file

@ -1228,13 +1228,19 @@ const initializeClient = async ({
req.user.id !== '' &&
typeof conversationId === 'string' &&
conversationId !== ''
? buildSubagentThreadTaskConfig(subagentThreadTaskStore, {
userId: req.user.id,
parentConversationId: conversationId,
...(typeof req.user.tenantId === 'string' && req.user.tenantId !== ''
? { tenantId: req.user.tenantId }
: {}),
})
? buildSubagentThreadTaskConfig(
subagentThreadTaskStore,
{
userId: req.user.id,
parentConversationId: conversationId,
...(typeof req.user.tenantId === 'string' && req.user.tenantId !== ''
? { tenantId: req.user.tenantId }
: {}),
},
{
completionWakeups: subagentThreadTaskStore.completionWakeupsEnabled === true,
},
)
: undefined;
let hasExistingSubagentTask = false;
if (trustedSubagentTasks != null && !(subagentsAvailableForRun && hasSpawnableSubagent)) {

View file

@ -84,4 +84,5 @@ async function configureSubagentTaskRouting() {
}
module.exports = subagentThreadTaskStore;
module.exports.completionWakeupsEnabled = completionWakeupsEnabled;
module.exports.configureSubagentTaskRouting = configureSubagentTaskRouting;

View file

@ -1,4 +1,6 @@
import type { SubagentTaskConfig } from '@librechat/agents';
import type { HostSubagentTaskConfig } from '~/agents/subagentDelivery';
import { SUBAGENT_COMPLETION_DELIVERY } from '~/agents/subagentDelivery';
import { CHECK_BACKGROUND_TASK_NAME } from '~/agents/background';
import { createRun } from '~/agents/run';
@ -58,7 +60,7 @@ jest.mock('~/agents/checkpointer', () => ({
getAgentCheckpointer: jest.fn().mockResolvedValue({}),
}));
import { InMemorySubagentTaskStore, Run } from '@librechat/agents';
import { HookRegistry, InMemorySubagentTaskStore, Run } from '@librechat/agents';
function makeAgent(overrides?: Record<string, unknown>) {
return {
@ -152,4 +154,44 @@ describe('createRun code-tool eager/session wiring', () => {
).not.toContain(CHECK_BACKGROUND_TASK_NAME);
expect(selfConfig.agentInputs?.toolRegistry?.has(CHECK_BACKGROUND_TASK_NAME)).toBe(false);
});
it('registers wakeup-aware schema and handle guidance for automatic subagent delivery', async () => {
const subagentTasks: HostSubagentTaskConfig = {
store: new InMemorySubagentTaskStore(),
scopeId: 'owner:wakeup-parent',
completionDelivery: SUBAGENT_COMPLETION_DELIVERY,
};
const runConfig = await captureRunConfig(
makeAgent({
subagents: { enabled: true, allowSelf: true },
toolDefinitions: [],
toolRegistry: new Map(),
}),
subagentTasks,
);
const [agentInput] = (runConfig.graphConfig as { agents: Array<Record<string, unknown>> })
.agents;
const poll = (agentInput.toolDefinitions as Array<{ name: string; description: string }>).find(
(definition) => definition.name === CHECK_BACKGROUND_TASK_NAME,
);
expect(poll?.description).toContain('automatic completion delivery');
const hooks = runConfig.hooks as HookRegistry;
const [matcher] = hooks.getMatchers('PostToolUse');
expect(matcher.pattern).toBe('subagent');
const result = await matcher.hooks[0](
{
hook_event_name: 'PostToolUse',
runId: 'run-1',
toolName: 'subagent',
toolInput: {},
toolOutput: JSON.stringify({ background_task_id: 'task-1', status: 'running' }),
toolUseId: 'call-1',
},
new AbortController().signal,
);
expect(JSON.parse(result.updatedOutput as string).message).toContain(
'the host will resume you',
);
});
});

View file

@ -1,6 +1,12 @@
import { logger } from '@librechat/data-schemas';
import { InMemorySubagentTaskStore } from '@librechat/agents';
import type { LCTool, LCToolRegistry, SubagentTaskConfig } from '@librechat/agents';
import type {
LCTool,
LCToolRegistry,
SubagentTaskConfig,
SubagentTaskRuntime,
} from '@librechat/agents';
import type { HostSubagentTaskConfig } from './subagentDelivery';
import {
isBackgroundEligibleToolName,
isBackgroundRequested,
@ -19,6 +25,7 @@ import {
CHECK_BACKGROUND_TASK_NAME,
RUN_IN_BACKGROUND_ARG,
} from './background';
import { SUBAGENT_COMPLETION_DELIVERY, SUBAGENT_WAKEUP_GUIDANCE } from './subagentDelivery';
import { SubagentTaskOwnerUnavailableError } from './subagentTaskRouting';
import { TOOL_SELECTION_WILDCARD } from './selection';
import { toolOptionsSchema } from './validation';
@ -306,6 +313,26 @@ describe('registerBackgroundTaskTool', () => {
collidingDef.description,
);
});
it('advertises automatic delivery only for wakeup-enabled subagents', () => {
const registry: LCToolRegistry = new Map();
const manual = registerBackgroundTaskTool({ toolRegistry: registry, toolDefinitions: [] });
const manualDescription = manual.toolDefinitions[0].description ?? '';
expect(manualDescription).toContain('Results are not pushed to you');
const automatic = registerBackgroundTaskTool({
toolRegistry: registry,
toolDefinitions: manual.toolDefinitions,
subagentCompletionWakeups: true,
});
expect(automatic.toolDefinitions).toHaveLength(1);
expect(automatic.toolDefinitions[0].description).toContain(
'Detached subagent tasks use automatic completion delivery',
);
expect(automatic.toolDefinitions[0].description).toContain(
'Ordinary background tool tasks require polling',
);
});
});
describe('synthesizeBackgroundToolOptions', () => {
@ -1291,6 +1318,97 @@ describe('runCheckBackgroundTask (singleton)', () => {
expect(second.result).toBeUndefined();
});
it('tells a wakeup-enabled parent to yield on an unchanged running subagent', async () => {
const store = new InMemorySubagentTaskStore();
const subagentTasks: HostSubagentTaskConfig = {
store,
scopeId: 'owner:wakeup-parent',
completionDelivery: SUBAGENT_COMPLETION_DELIVERY,
};
const started = store.start({
scopeId: subagentTasks.scopeId,
idempotencyKey: 'parent-run:parent-agent:call-wakeup',
parentRunId: 'parent-run',
parentAgentId: 'parent-agent',
parentToolCallId: 'call-wakeup',
input: 'Research this.',
subagentKind: 'agent',
subagentType: 'researcher',
run: (runtime: SubagentTaskRuntime) =>
new Promise((_, reject) => {
runtime.signal.addEventListener('abort', () => reject(runtime.signal.reason), {
once: true,
});
}),
});
if (!started.accepted) {
throw new Error('Expected subagent task to start.');
}
const polled = JSON.parse(
await runCheckBackgroundTask({
userId: 'owner',
conversationId: 'wakeup-parent',
args: { background_task_id: started.task.taskId },
subagentTasks,
}),
);
expect(polled).toMatchObject({
status: 'running',
message: SUBAGENT_WAKEUP_GUIDANCE,
});
const listed = JSON.parse(
await runCheckBackgroundTask({
userId: 'owner',
conversationId: 'wakeup-parent',
args: {},
subagentTasks,
}),
);
expect(listed.message).toBe(SUBAGENT_WAKEUP_GUIDANCE);
expect(listed.tasks[0].message).toBeUndefined();
store.control(subagentTasks.scopeId, started.task.taskId, { action: 'cancel' });
});
it('preserves poll-first running status when automatic delivery is disabled', async () => {
const store = new InMemorySubagentTaskStore();
const subagentTasks: SubagentTaskConfig = { store, scopeId: 'owner:manual-parent' };
const started = store.start({
scopeId: subagentTasks.scopeId,
idempotencyKey: 'parent-run:parent-agent:call-manual',
parentRunId: 'parent-run',
parentAgentId: 'parent-agent',
parentToolCallId: 'call-manual',
input: 'Research this.',
subagentKind: 'agent',
subagentType: 'researcher',
run: (runtime: SubagentTaskRuntime) =>
new Promise((_, reject) => {
runtime.signal.addEventListener('abort', () => reject(runtime.signal.reason), {
once: true,
});
}),
});
if (!started.accepted) {
throw new Error('Expected subagent task to start.');
}
const polled = JSON.parse(
await runCheckBackgroundTask({
userId: 'owner',
conversationId: 'manual-parent',
args: { background_task_id: started.task.taskId },
subagentTasks,
}),
);
expect(polled).toMatchObject({ status: 'running' });
expect(polled.message).toBeUndefined();
store.control(subagentTasks.scopeId, started.task.taskId, { action: 'cancel' });
});
it('routes parent control actions only to detached subagent tasks', async () => {
const store = new InMemorySubagentTaskStore();
const subagentTasks: SubagentTaskConfig = { store, scopeId: 'owner:parent-thread' };

View file

@ -56,6 +56,7 @@ import {
warnUnmatchedSelectionNames,
synthesizeSelectionToolOptions,
} from './selection';
import { SUBAGENT_WAKEUP_GUIDANCE, usesSubagentCompletionWakeups } from './subagentDelivery';
import { SubagentTaskOwnerUnavailableError } from './subagentTaskRouting';
import { SET_MEMORY_TOOL_NAME, DELETE_MEMORY_TOOL_NAME } from './memory';
import { ASK_USER_QUESTION_TOOL_NAME } from './hitl/askUserQuestionTool';
@ -307,6 +308,16 @@ const CHECK_BACKGROUND_TASK_DESCRIPTION = `Check, control, and retrieve tool or
Provide a background_task_id to poll one task; omit it to list every background task in this thread. A task is only finished when its status is "completed", "error", or "cancelled" never assume completion without polling. Results are not pushed to you; you must call this tool to collect them. Subagent tasks additionally accept steer, queue, interrupt, cancel, and cancel_message actions while running. Live subagent controls route across API replicas but do not survive a restart of the process that owns the executor. A completed subagent thread may be continued later through the subagent tool's durable thread id.`;
const CHECK_BACKGROUND_TASK_WAKEUP_DESCRIPTION = `Check, control, and retrieve tool or subagent tasks previously dispatched in the background (with run_in_background: true).
Provide a background_task_id to inspect one task; omit it to list every background task in this thread. Ordinary background tool tasks require polling to retrieve their results. Detached subagent tasks use automatic completion delivery: continue independent work or end the turn instead of repeatedly polling an unchanged running task, and the host will resume you when one finishes. Use this tool for explicit status, steer, queue, interrupt, cancel, or cancel_message actions, or as a fallback if automatic delivery is unavailable. Live subagent controls route across API replicas but do not survive a restart of the process that owns the executor. A completed subagent thread may be continued later through the subagent tool's durable thread id.`;
function checkBackgroundTaskDescription(subagentCompletionWakeups: boolean): string {
return subagentCompletionWakeups
? CHECK_BACKGROUND_TASK_WAKEUP_DESCRIPTION
: CHECK_BACKGROUND_TASK_DESCRIPTION;
}
/**
* `maxLength` is valid JSON Schema and is honored by providers, but the SDK's
* `JsonSchemaType` does not declare it, so the model-facing bounds are typed here.
@ -357,11 +368,13 @@ const CHECK_BACKGROUND_TASK_PARAMETERS = Object.freeze<CheckBackgroundTaskParame
required: [],
});
const CHECK_BACKGROUND_TASK_DEF: LCTool = Object.freeze<LCTool>({
name: CHECK_BACKGROUND_TASK_NAME,
description: CHECK_BACKGROUND_TASK_DESCRIPTION,
parameters: CHECK_BACKGROUND_TASK_PARAMETERS,
});
function buildCheckBackgroundTaskDefinition(subagentCompletionWakeups: boolean): LCTool {
return {
name: CHECK_BACKGROUND_TASK_NAME,
description: checkBackgroundTaskDescription(subagentCompletionWakeups),
parameters: CHECK_BACKGROUND_TASK_PARAMETERS,
};
}
/**
* Idempotently registers the `check_background_task` poll tool into the run's
@ -370,17 +383,23 @@ const CHECK_BACKGROUND_TASK_DEF: LCTool = Object.freeze<LCTool>({
export function registerBackgroundTaskTool(params: {
toolRegistry: LCToolRegistry | undefined;
toolDefinitions: LCTool[] | undefined;
subagentCompletionWakeups?: boolean;
}): { toolDefinitions: LCTool[] } {
const { toolRegistry, toolDefinitions } = params;
const { toolRegistry, toolDefinitions, subagentCompletionWakeups = false } = params;
const defs = toolDefinitions ?? [];
const desiredDescription = checkBackgroundTaskDescription(subagentCompletionWakeups);
const isOurs = (tool?: { description?: string }): boolean =>
tool?.description === CHECK_BACKGROUND_TASK_DESCRIPTION;
tool?.description === CHECK_BACKGROUND_TASK_DESCRIPTION ||
tool?.description === CHECK_BACKGROUND_TASK_WAKEUP_DESCRIPTION;
const existingDef = defs.find((d) => d.name === CHECK_BACKGROUND_TASK_NAME);
const existingRegistry = toolRegistry?.get(CHECK_BACKGROUND_TASK_NAME);
/** Already registered by us — idempotent no-op. */
if (isOurs(existingDef) || isOurs(existingRegistry)) {
if (
existingDef?.description === desiredDescription &&
(existingRegistry == null || existingRegistry.description === desiredDescription)
) {
return { toolDefinitions: defs };
}
@ -392,21 +411,29 @@ export function registerBackgroundTaskTool(params: {
* and warn that the colliding tool is shadowed.
*/
const collides = existingDef != null || existingRegistry != null;
if (collides) {
const foreignCollision =
(existingDef != null && !isOurs(existingDef)) ||
(existingRegistry != null && !isOurs(existingRegistry));
if (foreignCollision) {
logger.warn(
`[background] A tool named "${CHECK_BACKGROUND_TASK_NAME}" collides with the reserved background poll tool; the host poll tool takes precedence and the colliding tool is shadowed for this run.`,
);
}
toolRegistry?.set(CHECK_BACKGROUND_TASK_NAME, {
name: CHECK_BACKGROUND_TASK_NAME,
description: CHECK_BACKGROUND_TASK_DESCRIPTION,
description: desiredDescription,
parameters: CHECK_BACKGROUND_TASK_PARAMETERS,
allowed_callers: ['direct'],
});
const withoutCollision = collides
? defs.filter((d) => d.name !== CHECK_BACKGROUND_TASK_NAME)
: defs;
return { toolDefinitions: [...withoutCollision, CHECK_BACKGROUND_TASK_DEF] };
return {
toolDefinitions: [
...withoutCollision,
buildCheckBackgroundTaskDefinition(subagentCompletionWakeups),
],
};
}
/**
@ -1053,7 +1080,12 @@ interface SerializedSubagentTask {
function serializeSubagentSnapshot(
task: SubagentTaskSnapshot,
options: { includeResult?: string; status?: string; controlId?: string } = {},
options: {
includeResult?: string;
status?: string;
controlId?: string;
completionWakeups?: boolean;
} = {},
): SerializedSubagentTask {
return {
background_task_id: task.taskId,
@ -1069,10 +1101,16 @@ function serializeSubagentSnapshot(
...(task.pendingControls > 0 ? { pending_controls: task.pendingControls } : {}),
...(task.error == null ? {} : { error: task.error }),
...(options.controlId == null ? {} : { control_id: options.controlId }),
...(options.completionWakeups === true && task.status === 'running'
? { message: SUBAGENT_WAKEUP_GUIDANCE }
: {}),
};
}
function serializeSubagentClaim(claim: SubagentTaskClaim): SerializedSubagentTask | undefined {
function serializeSubagentClaim(
claim: SubagentTaskClaim,
completionWakeups: boolean,
): SerializedSubagentTask | undefined {
if (claim.status === 'not_found') {
return undefined;
}
@ -1085,7 +1123,7 @@ function serializeSubagentClaim(claim: SubagentTaskClaim): SerializedSubagentTas
error: claim.error,
};
}
return serializeSubagentSnapshot(claim.task, { status: claim.status });
return serializeSubagentSnapshot(claim.task, { status: claim.status, completionWakeups });
}
function serializeSubagentControl(
@ -1216,7 +1254,10 @@ export async function runCheckBackgroundTask(params: {
routedStore == null
? subagentTasks.store.claim(subagentTasks.scopeId, taskId)
: await routedStore.claimTask(subagentTasks.scopeId, taskId, invocationId);
const claimed = serializeSubagentClaim(claim);
const claimed = serializeSubagentClaim(
claim,
usesSubagentCompletionWakeups(subagentTasks),
);
if (claimed != null) {
return JSON.stringify(claimed);
}
@ -1267,6 +1308,7 @@ export async function runCheckBackgroundTask(params: {
const tasks = backgroundTaskRegistry.list(userId, conversationId);
let subagentTasks: SerializedSubagentTask[] = [];
let listWarning: string | undefined;
const completionWakeups = usesSubagentCompletionWakeups(params.subagentTasks);
if (params.subagentTasks != null) {
try {
const routedStore = routedSubagentStore(params.subagentTasks.store);
@ -1297,6 +1339,9 @@ export async function runCheckBackgroundTask(params: {
...tasks.map((task) => serializeTask(task, { includeResult: false })),
...subagentTasks,
],
...(completionWakeups && subagentTasks.some((task) => task.status === 'running')
? { message: SUBAGENT_WAKEUP_GUIDANCE }
: {}),
...(listWarning != null && { partial: true, warning: listWarning }),
});
}

View file

@ -50,5 +50,6 @@ export * from './steering';
export * from './triggers';
export * from './activityLabels';
export * from './activityPhases';
export * from './subagentDelivery';
export * from './reasoningLabels';
export * from './toolValidation';

View file

@ -53,6 +53,10 @@ import {
ASK_USER_QUESTION_TOOL_NAME,
createAskUserQuestionTool,
} from '~/agents/hitl/askUserQuestionTool';
import {
createSubagentWakeupHandleHook,
usesSubagentCompletionWakeups,
} from '~/agents/subagentDelivery';
import { resolveToolApprovalPolicy, exemptAskUserQuestionFromApproval } from '~/agents/hitl/policy';
import { applyCustomHandoffPromptKeyCompatibility } from '~/agents/handoffPromptKeyCompatibility';
import { stripIntentFromToolRegistry, stripIntentFromToolDefinitions } from '~/agents/intent';
@ -1632,6 +1636,7 @@ export async function createRun({
agentInput.toolDefinitions = registerBackgroundTaskTool({
toolRegistry: agentInput.toolRegistry,
toolDefinitions: agentInput.toolDefinitions,
subagentCompletionWakeups: usesSubagentCompletionWakeups(subagentTasks),
}).toolDefinitions;
}
agentInputs.push(agentInput);
@ -1729,6 +1734,14 @@ export async function createRun({
* this guard is defense in depth).
*/
let hooks = hitl?.hooks;
if (usesSubagentCompletionWakeups(subagentTasks)) {
hooks = hooks ?? new HookRegistry();
hooks.register('PostToolUse', {
pattern: String(Constants.SUBAGENT),
hooks: [createSubagentWakeupHandleHook()],
internal: true,
});
}
/** Activity labels register BEFORE the steer drain: the label must claim
* its slot while the batch's tool parts are still the content tail. If a
* steer drained first, its injected part would flush the tool block in

View file

@ -0,0 +1,63 @@
import type { PostToolUseHookInput } from '@librechat/agents';
import { SUBAGENT_WAKEUP_GUIDANCE, createSubagentWakeupHandleHook } from './subagentDelivery';
const hookSignal = new AbortController().signal;
function input(toolName: string, toolOutput: unknown): PostToolUseHookInput {
return {
hook_event_name: 'PostToolUse',
toolName,
toolInput: {},
toolOutput,
toolUseId: 'call-1',
} as PostToolUseHookInput;
}
describe('createSubagentWakeupHandleHook', () => {
it('replaces the legacy poll-first instruction on a running detached subagent handle', async () => {
const output = JSON.stringify({
background_task_id: 'task-1',
subagent_thread_id: 'thread-1',
tool: 'subagent',
subagent_type: 'researcher',
status: 'running',
message: 'Poll the host background-task tool.',
});
const result = await createSubagentWakeupHandleHook()(input('subagent', output), hookSignal);
const updated = JSON.parse(result.updatedOutput as string);
expect(updated).toMatchObject({
background_task_id: 'task-1',
subagent_thread_id: 'thread-1',
status: 'running',
message: SUBAGENT_WAKEUP_GUIDANCE,
});
});
it('leaves ordinary background tools and terminal subagent results unchanged', async () => {
const hook = createSubagentWakeupHandleHook();
await expect(
hook(
input('execute_code', JSON.stringify({ background_task_id: 'code-1', status: 'running' })),
hookSignal,
),
).resolves.toEqual({});
await expect(
hook(
input('subagent', JSON.stringify({ background_task_id: 'task-1', status: 'completed' })),
hookSignal,
),
).resolves.toEqual({});
});
it('fails closed on malformed or non-handle subagent output', async () => {
const hook = createSubagentWakeupHandleHook();
await expect(hook(input('subagent', 'not-json'), hookSignal)).resolves.toEqual({});
await expect(
hook(input('subagent', JSON.stringify({ status: 'running' })), hookSignal),
).resolves.toEqual({});
});
});

View file

@ -0,0 +1,59 @@
import { Constants } from '@librechat/agents';
import type { HookCallback, PostToolUseHookOutput, SubagentTaskConfig } from '@librechat/agents';
export const SUBAGENT_COMPLETION_DELIVERY = 'wakeup';
/** Host-owned detached-subagent scope with its model-facing result-delivery contract. */
export interface HostSubagentTaskConfig extends SubagentTaskConfig {
completionDelivery?: typeof SUBAGENT_COMPLETION_DELIVERY;
}
export const SUBAGENT_WAKEUP_GUIDANCE =
'Automatic completion delivery is enabled for this subagent task. Continue independent work if available; otherwise end this turn and the host will resume you when the task finishes. Do not repeatedly poll an unchanged running task. Use check_background_task only for explicit status or control, or as a fallback if automatic delivery is unavailable.';
export function usesSubagentCompletionWakeups(
config: SubagentTaskConfig | undefined,
): config is HostSubagentTaskConfig {
return (
(config as HostSubagentTaskConfig | undefined)?.completionDelivery ===
SUBAGENT_COMPLETION_DELIVERY
);
}
function parseOutput(output: unknown): Record<string, unknown> | undefined {
if (typeof output === 'object' && output !== null && !Array.isArray(output)) {
return output as Record<string, unknown>;
}
if (typeof output !== 'string') {
return undefined;
}
try {
const parsed = JSON.parse(output) as unknown;
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
? (parsed as Record<string, unknown>)
: undefined;
} catch {
return undefined;
}
}
/** Replaces the SDK's legacy poll-first handle with the host's durable delivery contract. */
export function createSubagentWakeupHandleHook(): HookCallback<'PostToolUse'> {
return async (input): Promise<PostToolUseHookOutput> => {
if (input.toolName !== String(Constants.SUBAGENT)) {
return {};
}
const output = parseOutput(input.toolOutput);
if (
output?.status !== 'running' ||
typeof output.background_task_id !== 'string' ||
output.background_task_id === ''
) {
return {};
}
const updated = { ...output, message: SUBAGENT_WAKEUP_GUIDANCE };
return {
updatedOutput: typeof input.toolOutput === 'string' ? JSON.stringify(updated) : updated,
};
};
}

View file

@ -34,6 +34,7 @@ import {
SubagentThreadTaskStore,
} from './subagentThreads';
import { SubagentTaskOwnerUnavailableError } from './subagentTaskRouting';
import { SUBAGENT_COMPLETION_DELIVERY } from './subagentDelivery';
import { createSubagentAttemptKey } from './subagentThreadIds';
import { createSubagentUsageSink } from './usage';
@ -253,6 +254,16 @@ beforeEach(async () => {
});
describe('SubagentThreadTaskStore', () => {
it('records the host-selected automatic delivery contract only when wakeups are enabled', () => {
const store = new SubagentThreadTaskStore(methods);
const scope = { userId: 'delivery-user', parentConversationId: randomUUID() };
expect(buildSubagentThreadTaskConfig(store, scope).completionDelivery).toBeUndefined();
expect(
buildSubagentThreadTaskConfig(store, scope, { completionWakeups: true }).completionDelivery,
).toBe(SUBAGENT_COMPLETION_DELIVERY);
});
it('maps one logical SDK thread to a durable, view-only LibreChat conversation', async () => {
const userId = 'user-1';
const parentConversationId = randomUUID();

View file

@ -9,7 +9,6 @@ import {
import type {
InMemorySubagentTaskStoreOptions,
SubagentTaskClaim,
SubagentTaskConfig,
SubagentTaskControlCommand,
SubagentTaskControlResult,
SubagentTaskRuntime,
@ -29,6 +28,7 @@ import type {
import type { BaseMessage, StoredMessage } from '@librechat/agents/langchain/messages';
import type { SubagentTaskControlTransport } from './subagentTaskRouting';
import type { UsageMetadata } from '~/stream/interfaces/IJobStore';
import type { HostSubagentTaskConfig } from './subagentDelivery';
import {
boundedClaim,
boundedTaskList,
@ -37,6 +37,7 @@ import {
} from './subagentTaskRouting';
import { createSubagentAttemptKey, createSubagentThreadId } from './subagentThreadIds';
import { runWithDetachedSubagentUsage } from './subagentTaskContext';
import { SUBAGENT_COMPLETION_DELIVERY } from './subagentDelivery';
import { createConcurrencyLimiter } from '~/utils/promise';
import { aggregateEmittedUsage } from './usage';
@ -2174,9 +2175,13 @@ export function createSubagentThreadTaskStore(
export function buildSubagentThreadTaskConfig(
store: SubagentThreadTaskStore,
scope: Omit<SubagentThreadScope, 'version'>,
): SubagentTaskConfig {
options: { completionWakeups?: boolean } = {},
): HostSubagentTaskConfig {
return {
store,
scopeId: serializeScope(scope),
...(options.completionWakeups === true
? { completionDelivery: SUBAGENT_COMPLETION_DELIVERY }
: {}),
};
}