⚙️ 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",
"@keyv/mongo": "^2.1.8",
"@keyv/redis": "^2.8.1",
"@langchain/community": "^0.3.34",
"@langchain/core": "^0.3.40",
"@langchain/google-genai": "^0.1.11",
"@langchain/google-vertexai": "^0.2.2",
"@langchain/community": "^0.3.39",
"@langchain/core": "^0.3.43",
"@langchain/google-genai": "^0.2.2",
"@langchain/google-vertexai": "^0.2.3",
"@langchain/textsplitters": "^0.1.0",
"@librechat/agents": "^2.3.95",
"@librechat/agents": "^2.4.0",
"@librechat/data-schemas": "*",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.8.2",

View file

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

View file

@ -5,7 +5,8 @@ import {
SandpackCodeEditor,
SandpackProvider as StyledProvider,
} 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 { ArtifactFiles, Artifact } from '~/common';
import { useEditArtifact, useGetStartupConfig } from '~/data-provider';
@ -66,7 +67,7 @@ const CodeEditor = ({
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()) {
setCurrentCode(currentCode);

View file

@ -123,11 +123,14 @@ export default function useStepHandler({
} else if (contentType === ContentTypes.TOOL_CALL && 'tool_call' in contentPart) {
const existingContent = updatedContent[index] as Agents.ToolCallContent | undefined;
const existingToolCall = existingContent?.tool_call;
const toolCallArgs = (contentPart.tool_call.args as unknown as string | undefined) ?? '';
const args = finalUpdate
? contentPart.tool_call.args
: (existingToolCall?.args ?? '') + toolCallArgs;
const toolCallArgs = (contentPart.tool_call as Agents.ToolCall).args;
/** When args are a valid object, they are likely already invoked */
const args =
finalUpdate ||
typeof existingToolCall?.args === 'object' ||
typeof toolCallArgs === 'object'
? contentPart.tool_call.args
: (existingToolCall?.args ?? '') + (toolCallArgs ?? '');
const id = getNonEmptyValue([contentPart.tool_call.id, existingToolCall?.id]) ?? '';
const name = getNonEmptyValue([contentPart.tool_call.name, existingToolCall?.name]) ?? '';
@ -195,12 +198,31 @@ export default function useStepHandler({
// Store tool call IDs if present
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 ?? '';
if ('id' in toolCall && 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') {
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
*/
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);
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) {
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));
@ -69,7 +74,7 @@ export class FlowStateManager<T = unknown> {
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) {
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
return this.monitorFlow(flowKey, type);
return this.monitorFlow(flowKey, type, signal);
}
const initialState: FlowState = {
@ -81,10 +86,10 @@ export class FlowStateManager<T = unknown> {
this.logger.debug('Creating initial flow state:', flowKey);
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) => {
const checkInterval = 2000;
let elapsedTime = 0;
@ -101,6 +106,16 @@ export class FlowStateManager<T = unknown> {
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') {
clearInterval(intervalId);
this.intervals.delete(intervalId);
@ -197,19 +212,19 @@ export class FlowStateManager<T = unknown> {
* @param flowId - The ID of the flow
* @param type - The type of flow
* @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(
flowId: string,
type: string,
handler: () => Promise<T>,
metadata: FlowMetadata = {},
signal?: AbortSignal,
): Promise<T> {
const flowKey = this.getFlowKey(flowId, type);
let existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) {
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));
@ -217,13 +232,13 @@ export class FlowStateManager<T = unknown> {
existingState = (await this.keyv.get(flowKey)) as FlowState<T> | undefined;
if (existingState) {
this.logger.debug(`[${flowKey}] Flow exists on 2nd check`);
return this.monitorFlow(flowKey, type);
return this.monitorFlow(flowKey, type, signal);
}
const initialState: FlowState = {
type,
status: 'PENDING',
metadata,
metadata: {},
createdAt: Date.now(),
};
this.logger.debug(`[${flowKey}] Creating initial flow state`);