⚙️ refactor: OAuth Flow Signal, Type Safety, Tool Progress & Updated Packages (#6752)

* chore: bump @librechat/agents and related packages

* refactor: update message state for tool calls run step, in case no tool call chunks are received

* fix: avoid combining finalized args createContentAggregator for tool calls

* chore: bump @librechat/agents to version 2.3.99

* feat: add support for aborting flows with AbortSignal in createFlow methods

* fix: improve handling of tool call arguments in useStepHandler

* chore: bump @librechat/agents to version 2.4.0

* fix: update flow identifier format for OAuth login in createActionTool to allow uniqueness per run

* fix: improve error message handling for aborted flows in FlowStateManager

* refactor: allow possible multi-agent cross-over for oauth login

* fix: add type safety for Sandpack files in ArtifactCodeEditor
This commit is contained in:
Danny Avila 2025-04-06 03:28:05 -04:00 committed by GitHub
parent ac35b8490c
commit 9b0678da16
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 953 additions and 981 deletions

View file

@ -44,12 +44,12 @@
"@googleapis/youtube": "^20.0.0", "@googleapis/youtube": "^20.0.0",
"@keyv/mongo": "^2.1.8", "@keyv/mongo": "^2.1.8",
"@keyv/redis": "^2.8.1", "@keyv/redis": "^2.8.1",
"@langchain/community": "^0.3.34", "@langchain/community": "^0.3.39",
"@langchain/core": "^0.3.40", "@langchain/core": "^0.3.43",
"@langchain/google-genai": "^0.1.11", "@langchain/google-genai": "^0.2.2",
"@langchain/google-vertexai": "^0.2.2", "@langchain/google-vertexai": "^0.2.3",
"@langchain/textsplitters": "^0.1.0", "@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.3.95", "@librechat/agents": "^2.4.0",
"@librechat/data-schemas": "*", "@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1", "@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2", "axios": "^1.8.2",

View file

@ -191,16 +191,20 @@ async function createActionTool({
}; };
const flowManager = await getFlowStateManager(getLogStores); const flowManager = await getFlowStateManager(getLogStores);
await flowManager.createFlowWithHandler( await flowManager.createFlowWithHandler(
`${identifier}:login`, `${identifier}:oauth_login:${config.metadata.thread_id}:${config.metadata.run_id}`,
'oauth_login', 'oauth_login',
async () => { async () => {
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data }); sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
logger.debug('Sent OAuth login request to client', { action_id, identifier }); logger.debug('Sent OAuth login request to client', { action_id, identifier });
return true; return true;
}, },
config?.signal,
); );
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier }); logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
const result = await flowManager.createFlow(identifier, 'oauth', { const result = await flowManager.createFlow(
identifier,
'oauth',
{
state: stateToken, state: stateToken,
userId: req.user.id, userId: req.user.id,
client_url: metadata.auth.client_url, client_url: metadata.auth.client_url,
@ -208,7 +212,9 @@ async function createActionTool({
/** Encrypted values */ /** Encrypted values */
encrypted_oauth_client_id: encrypted.oauth_client_id, encrypted_oauth_client_id: encrypted.oauth_client_id,
encrypted_oauth_client_secret: encrypted.oauth_client_secret, encrypted_oauth_client_secret: encrypted.oauth_client_secret,
}); },
config?.signal,
);
logger.debug('Received OAuth Authorization response', { action_id, identifier }); logger.debug('Received OAuth Authorization response', { action_id, identifier });
data.delta.auth = undefined; data.delta.auth = undefined;
data.delta.expires_at = undefined; data.delta.expires_at = undefined;
@ -264,6 +270,7 @@ async function createActionTool({
`${identifier}:refresh`, `${identifier}:refresh`,
'oauth_refresh', 'oauth_refresh',
refreshTokens, refreshTokens,
config?.signal,
); );
metadata.oauth_access_token = refreshData.access_token; metadata.oauth_access_token = refreshData.access_token;
if (refreshData.refresh_token) { if (refreshData.refresh_token) {

View file

@ -5,7 +5,8 @@ import {
SandpackCodeEditor, SandpackCodeEditor,
SandpackProvider as StyledProvider, SandpackProvider as StyledProvider,
} from '@codesandbox/sandpack-react'; } from '@codesandbox/sandpack-react';
import { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled'; import type { SandpackProviderProps } from '@codesandbox/sandpack-react/unstyled';
import type { SandpackBundlerFile } from '@codesandbox/sandpack-client';
import type { CodeEditorRef } from '@codesandbox/sandpack-react'; import type { CodeEditorRef } from '@codesandbox/sandpack-react';
import type { ArtifactFiles, Artifact } from '~/common'; import type { ArtifactFiles, Artifact } from '~/common';
import { useEditArtifact, useGetStartupConfig } from '~/data-provider'; import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
@ -66,7 +67,7 @@ const CodeEditor = ({
return; return;
} }
const currentCode = sandpack.files['/' + fileKey].code; const currentCode = (sandpack.files['/' + fileKey] as SandpackBundlerFile | undefined)?.code;
if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) { if (currentCode && artifact.content != null && currentCode.trim() !== artifact.content.trim()) {
setCurrentCode(currentCode); setCurrentCode(currentCode);

View file

@ -123,11 +123,14 @@ export default function useStepHandler({
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) { } else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined; const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
const existingToolCall = existingContent?.tool_call; const existingToolCall = existingContent?.tool_call;
const toolCallArgs = (contentPart.tool_call.args as unknown as string | undefined) ?? ''; const toolCallArgs = (contentPart.tool_call as Agents.ToolCall).args;
/** When args are a valid object, they are likely already invoked */
const args = finalUpdate const args =
finalUpdate ||
typeof existingToolCall?.args === 'object' ||
typeof toolCallArgs === 'object'
? contentPart.tool_call.args ? contentPart.tool_call.args
: (existingToolCall?.args ?? '') + toolCallArgs; : (existingToolCall?.args ?? '') + (toolCallArgs ?? '');
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? ''; const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? ''; const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
@ -195,12 +198,31 @@ export default function useStepHandler({
// Store tool call IDs if present // Store tool call IDs if present
if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) { if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) {
runStep.stepDetails.tool_calls.forEach((toolCall) => { let updatedResponse = { ...response };
(runStep.stepDetails.tool_calls as Agents.ToolCall[]).forEach((toolCall) => {
const toolCallId = toolCall.id ?? ''; const toolCallId = toolCall.id ?? '';
if ('id' in toolCall && toolCallId) { if ('id' in toolCall && toolCallId) {
toolCallIdMap.current.set(runStep.id, toolCallId); toolCallIdMap.current.set(runStep.id, toolCallId);
} }
const contentPart: Agents.MessageContentComplex = {
type: ContentTypes.TOOL_CALL,
tool_call: {
name: toolCall.name ?? '',
args: toolCall.args,
id: toolCallId,
},
};
updatedResponse = updateContent(updatedResponse, runStep.index, contentPart);
}); });
messageMap.current.set(responseMessageId, updatedResponse);
const updatedMessages = messages.map((msg) =>
msg.messageId === runStep.runId ? updatedResponse : msg,
);
setMessages(updatedMessages);
} }
} else if (event === 'on_agent_update') { } else if (event === 'on_agent_update') {
const { agent_update } = data as Agents.AgentUpdate; const { agent_update } = data as Agents.AgentUpdate;

1823
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -55,13 +55,18 @@ export class FlowStateManager<T = unknown> {
/** /**
* Creates a new flow and waits for its completion * Creates a new flow and waits for its completion
*/ */
async createFlow(flowId: string, type: string, metadata: FlowMetadata = {}): Promise<T> { async createFlow(
flowId: string,
type: string,
metadata: FlowMetadata = {},
signal?: AbortSignal,
): Promise<T> {
const flowKey = this.getFlowKey(flowId, type); const flowKey = this.getFlowKey(flowId, type);
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined; let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) { if (existingState) {
this.logger.debug(`[${flowKey}] Flow already exists`); this.logger.debug(`[${flowKey}] Flow already exists`);
return this.monitorFlow(flowKey, type); return this.monitorFlow(flowKey, type, signal);
} }
await new Promise((resolve) => setTimeout(resolve, 250)); await new Promise((resolve) => setTimeout(resolve, 250));
@ -69,7 +74,7 @@ export class FlowStateManager<T = unknown> {
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined; existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) { if (existingState) {
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`); this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
return this.monitorFlow(flowKey, type); return this.monitorFlow(flowKey, type, signal);
} }
const initialState: FlowState = { const initialState: FlowState = {
@ -81,10 +86,10 @@ export class FlowStateManager<T = unknown> {
this.logger.debug('Creating initial flow state:', flowKey); this.logger.debug('Creating initial flow state:', flowKey);
await this.keyv.set(flowKey, initialState, this.ttl); await this.keyv.set(flowKey, initialState, this.ttl);
return this.monitorFlow(flowKey, type); return this.monitorFlow(flowKey, type, signal);
} }
private monitorFlow(flowKey: string, type: string): Promise<T> { private monitorFlow(flowKey: string, type: string, signal?: AbortSignal): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const checkInterval = 2000; const checkInterval = 2000;
let elapsedTime = 0; let elapsedTime = 0;
@ -101,6 +106,16 @@ export class FlowStateManager<T = unknown> {
return; return;
} }
if (signal?.aborted) {
clearInterval(intervalId);
this.intervals.delete(intervalId);
this.logger.warn(`[${flowKey}] Flow aborted`);
const message = `${type} flow aborted`;
await this.keyv.delete(flowKey);
reject(new Error(message));
return;
}
if (flowState.status !== 'PENDING') { if (flowState.status !== 'PENDING') {
clearInterval(intervalId); clearInterval(intervalId);
this.intervals.delete(intervalId); this.intervals.delete(intervalId);
@ -197,19 +212,19 @@ export class FlowStateManager<T = unknown> {
* @param flowId - The ID of the flow * @param flowId - The ID of the flow
* @param type - The type of flow * @param type - The type of flow
* @param handler - Async function to execute if no existing flow is found * @param handler - Async function to execute if no existing flow is found
* @param metadata - Optional metadata for the flow * @param signal - Optional AbortSignal to cancel the flow
*/ */
async createFlowWithHandler( async createFlowWithHandler(
flowId: string, flowId: string,
type: string, type: string,
handler: () => Promise<T>, handler: () => Promise<T>,
metadata: FlowMetadata = {}, signal?: AbortSignal,
): Promise<T> { ): Promise<T> {
const flowKey = this.getFlowKey(flowId, type); const flowKey = this.getFlowKey(flowId, type);
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined; let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) { if (existingState) {
this.logger.debug(`[${flowKey}] Flow already exists`); this.logger.debug(`[${flowKey}] Flow already exists`);
return this.monitorFlow(flowKey, type); return this.monitorFlow(flowKey, type, signal);
} }
await new Promise((resolve) => setTimeout(resolve, 250)); await new Promise((resolve) => setTimeout(resolve, 250));
@ -217,13 +232,13 @@ export class FlowStateManager<T = unknown> {
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined; existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) { if (existingState) {
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`); this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
return this.monitorFlow(flowKey, type); return this.monitorFlow(flowKey, type, signal);
} }
const initialState: FlowState = { const initialState: FlowState = {
type, type,
status: 'PENDING', status: 'PENDING',
metadata, metadata: {},
createdAt: Date.now(), createdAt: Date.now(),
}; };
this.logger.debug(`[${flowKey}] Creating initial flow state`); this.logger.debug(`[${flowKey}] Creating initial flow state`);