mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-29 12:22:40 +00:00
🪴 feat: Fork Completed Subagents Into Continuable Chats (#15133)
* feat: continue completed subagents as chats * chore: sort continuation imports
This commit is contained in:
parent
77cb72e50c
commit
1d9c2fc591
4 changed files with 225 additions and 8 deletions
|
|
@ -11,9 +11,15 @@ const BaseClient = require('~/app/clients/BaseClient');
|
|||
* Helper function to clone messages with proper parent-child relationships and timestamps
|
||||
* @param {TMessage[]} messagesToClone - Original messages to clone
|
||||
* @param {ImportBatchBuilder} importBatchBuilder - Instance of ImportBatchBuilder
|
||||
* @param {object} [options] - Clone behavior for the source conversation.
|
||||
* @param {boolean} [options.detachSubagentRuntime=false] - Remove durable child execution metadata.
|
||||
* @returns {Map<string, string>} Map of original messageIds to new messageIds
|
||||
*/
|
||||
function cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder) {
|
||||
function cloneMessagesWithTimestamps(
|
||||
messagesToClone,
|
||||
importBatchBuilder,
|
||||
{ detachSubagentRuntime = false } = {},
|
||||
) {
|
||||
const idMapping = new Map();
|
||||
|
||||
// First pass: create ID mapping and sort messages by parentMessageId
|
||||
|
|
@ -63,6 +69,10 @@ function cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder) {
|
|||
parentMessageId: parentId,
|
||||
createdAt,
|
||||
};
|
||||
if (detachSubagentRuntime) {
|
||||
delete clonedMessage.subagentTask;
|
||||
delete clonedMessage.subagentTranscript;
|
||||
}
|
||||
|
||||
importBatchBuilder.saveMessage(clonedMessage);
|
||||
}
|
||||
|
|
@ -136,7 +146,12 @@ async function forkConversation({
|
|||
messagesToClone = getMessagesUpToTargetLevel(originalMessages, targetMessageId);
|
||||
}
|
||||
|
||||
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);
|
||||
cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder, {
|
||||
/** A human continuation is an ordinary conversation snapshot, not another
|
||||
* durable child executor. Preserve visible history while dropping the
|
||||
* task protocol and private serialized model transcript. */
|
||||
detachSubagentRuntime: originalConvo.subagentThread != null,
|
||||
});
|
||||
|
||||
const result = importBatchBuilder.finishConversation(
|
||||
newTitle || originalConvo.title,
|
||||
|
|
|
|||
|
|
@ -178,6 +178,68 @@ describe('forkConversation', () => {
|
|||
});
|
||||
|
||||
expect(bulkSaveConvos.mock.calls[0][0][0]).not.toHaveProperty('subagentThread');
|
||||
expect(
|
||||
bulkSaveMessages.mock.calls[0][0].every(
|
||||
(message) => message.subagentTask == null && message.subagentTranscript == null,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('drops child execution metadata while retaining its visible transcript', async () => {
|
||||
getConvo.mockResolvedValue({
|
||||
...mockConversation,
|
||||
agent_id: 'agent-1',
|
||||
subagentThread: {
|
||||
rootConversationId: 'root-conversation',
|
||||
parentConversationId: 'parent-conversation',
|
||||
parentToolCallId: 'parent-tool-call',
|
||||
subagentType: 'researcher',
|
||||
subagentKind: 'agent',
|
||||
depth: 1,
|
||||
},
|
||||
});
|
||||
getMessages.mockResolvedValue([
|
||||
{
|
||||
messageId: 'task-1:user',
|
||||
parentMessageId: Constants.NO_PARENT,
|
||||
isCreatedByUser: true,
|
||||
text: 'Investigate this.',
|
||||
subagentTask: { attemptKey: 'attempt-1', status: 'running' },
|
||||
},
|
||||
{
|
||||
messageId: 'task-1:assistant',
|
||||
parentMessageId: 'task-1:user',
|
||||
isCreatedByUser: false,
|
||||
text: 'The investigation is complete.',
|
||||
subagentTask: { attemptKey: 'attempt-1', status: 'completed' },
|
||||
subagentTranscript: {
|
||||
taskId: 'task-1',
|
||||
mode: 'replace',
|
||||
messagesJson: '[{"role":"assistant","content":"private"}]',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await forkConversation({
|
||||
originalConvoId: 'abc123',
|
||||
targetMessageId: 'task-1:assistant',
|
||||
requestUserId: 'user1',
|
||||
option: ForkOptions.DIRECT_PATH,
|
||||
});
|
||||
|
||||
const savedConversation = bulkSaveConvos.mock.calls[0][0][0];
|
||||
const savedMessages = bulkSaveMessages.mock.calls[0][0];
|
||||
expect(savedConversation).toEqual(expect.objectContaining({ agent_id: 'agent-1' }));
|
||||
expect(savedConversation).not.toHaveProperty('subagentThread');
|
||||
expect(savedMessages.map((message) => message.text)).toEqual([
|
||||
'Investigate this.',
|
||||
'The investigation is complete.',
|
||||
]);
|
||||
expect(
|
||||
savedMessages.every(
|
||||
(message) => message.subagentTask == null && message.subagentTranscript == null,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should fork conversation with branches', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue