From 6a7da61234b0ded6d43284c875ba3b221f1f6b3b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 24 Aug 2026 08:38:30 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=B8=20chore:=20Resolve=20Agents=20SDK?= =?UTF-8?q?=20Path=20Aliases=20That=20Masked=20Backend=20Types=20(#15160)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix: Restore Agents SDK Type Resolution in Backend Type Checks * 🐛 fix: Preserve Typed Prompt Callback Assignability * 🐛 fix: Accept Agents Function Tool Calls in isImageVisionTool * 🐛 fix: Prove the Run Step Wire Contract at Compile Time --- client/src/hooks/SSE/useStepHandler.ts | 2 +- .../__tests__/graph-subagent.e2e.test.ts | 1 - .../__tests__/summarization.e2e.test.ts | 6 +- .../src/agents/activityPhases/runtime.spec.ts | 159 +++++++++--------- .../api/src/agents/activityPhases/runtime.ts | 5 +- packages/api/src/agents/hitl/resume.ts | 4 +- .../agents/reasoningLabels/runtime.spec.ts | 6 +- packages/api/src/agents/run.ts | 6 +- .../agents/steering/__tests__/offset.spec.ts | 41 ++++- .../agents/steering/__tests__/runtime.spec.ts | 16 +- packages/api/src/agents/subagentThreads.ts | 2 +- .../implementations/InMemoryJobStore.ts | 3 +- .../stream/implementations/RedisJobStore.ts | 3 +- .../api/src/stream/interfaces/IJobStore.ts | 53 +++++- packages/api/src/types/agents.d.ts | 16 -- packages/api/tsconfig.json | 6 +- packages/data-provider/src/schemas.ts | 11 +- packages/data-provider/src/types/agents.ts | 43 ++++- 18 files changed, 241 insertions(+), 142 deletions(-) delete mode 100644 packages/api/src/types/agents.d.ts diff --git a/client/src/hooks/SSE/useStepHandler.ts b/client/src/hooks/SSE/useStepHandler.ts index 934fad74a0..d7b91f855d 100644 --- a/client/src/hooks/SSE/useStepHandler.ts +++ b/client/src/hooks/SSE/useStepHandler.ts @@ -954,7 +954,7 @@ export default function useStepHandler({ // Store tool call IDs if present if (runStep.stepDetails.type === StepTypes.TOOL_CALLS) { let updatedResponse = { ...response }; - (runStep.stepDetails.tool_calls as Agents.ToolCall[]).forEach((toolCall) => { + ((runStep.stepDetails.tool_calls ?? []) as Agents.ToolCall[]).forEach((toolCall) => { const toolCallId = toolCall.id ?? ''; if ('id' in toolCall && toolCallId) { toolCallIdMap.current.set(runStep.id, toolCallId); diff --git a/packages/api/src/agents/__tests__/graph-subagent.e2e.test.ts b/packages/api/src/agents/__tests__/graph-subagent.e2e.test.ts index ebe067a743..d66f8c0a0f 100644 --- a/packages/api/src/agents/__tests__/graph-subagent.e2e.test.ts +++ b/packages/api/src/agents/__tests__/graph-subagent.e2e.test.ts @@ -139,7 +139,6 @@ liveDescribe('Graph subagent E2E (LibreChat)', () => { { configurable: { thread_id: runId }, recursionLimit: 100, - streamMode: 'values', version: 'v2', }, ); diff --git a/packages/api/src/agents/__tests__/summarization.e2e.test.ts b/packages/api/src/agents/__tests__/summarization.e2e.test.ts index 03ef2ca6d4..0a52354060 100644 --- a/packages/api/src/agents/__tests__/summarization.e2e.test.ts +++ b/packages/api/src/agents/__tests__/summarization.e2e.test.ts @@ -212,7 +212,11 @@ async function runFullTurn({ initialSummary, runId: `e2e-${Date.now()}`, signal: abortController.signal, - customHandlers: buildHandlers(collectedUsage, aggregateContent, spies) as never, + customHandlers: buildHandlers( + collectedUsage, + aggregateContent as (params: { event: string; data: unknown }) => void, + spies, + ) as never, summarizationConfig, tokenCounter, }); diff --git a/packages/api/src/agents/activityPhases/runtime.spec.ts b/packages/api/src/agents/activityPhases/runtime.spec.ts index f9291240d9..44a451c606 100644 --- a/packages/api/src/agents/activityPhases/runtime.spec.ts +++ b/packages/api/src/agents/activityPhases/runtime.spec.ts @@ -100,7 +100,7 @@ describe('createActivityPhaseWiring', () => { phase: 'final_answer', }, }, - }, + } as never, undefined, undefined, ); @@ -112,7 +112,7 @@ describe('createActivityPhaseWiring', () => { delta: { content: { type: ContentTypes.TEXT, text: 'A'.repeat(SUBSTANTIAL_TEXT_CHARS + 1) }, }, - }, + } as never, undefined, undefined, ); @@ -185,13 +185,13 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: id, content_type: 'text', ...(phase && { phase }) }, }, - }, + } as never, undefined, undefined, ); handlers?.[GraphEvents.ON_MESSAGE_DELTA]?.handle( GraphEvents.ON_MESSAGE_DELTA, - { id, delta: { content: { type: ContentTypes.TEXT, text } } }, + { id, delta: { content: { type: ContentTypes.TEXT, text } } } as never, undefined, undefined, ); @@ -274,13 +274,16 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); handlers?.[GraphEvents.ON_MESSAGE_DELTA]?.handle( GraphEvents.ON_MESSAGE_DELTA, - { id: 'interleaved-text', delta: { content: { type: ContentTypes.TEXT, text: 'prefix' } } }, + { + id: 'interleaved-text', + delta: { content: { type: ContentTypes.TEXT, text: 'prefix' } }, + } as never, undefined, undefined, ); @@ -291,7 +294,7 @@ describe('createActivityPhaseWiring', () => { { id: 'interleaved-text', delta: { content: { type: ContentTypes.TEXT, text: 'x'.repeat(SUBSTANTIAL_TEXT_CHARS) } }, - }, + } as never, undefined, undefined, ); @@ -348,7 +351,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -360,7 +363,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -370,7 +373,7 @@ describe('createActivityPhaseWiring', () => { { id: 'later-reasoning', delta: { content: { type: ContentTypes.THINK, think: 'Investigating the later tool.' } }, - }, + } as never, undefined, undefined, ); @@ -379,7 +382,7 @@ describe('createActivityPhaseWiring', () => { { id: 'boundary-text', delta: { content: { type: ContentTypes.TEXT, text: substantialText('Boundary result.') } }, - }, + } as never, undefined, undefined, ); @@ -547,14 +550,14 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); const emitTextDelta = (id: string, text: string) => handlers?.[GraphEvents.ON_MESSAGE_DELTA]?.handle( GraphEvents.ON_MESSAGE_DELTA, - { id, delta: { content: { type: ContentTypes.TEXT, text } } }, + { id, delta: { content: { type: ContentTypes.TEXT, text } } } as never, undefined, undefined, ); @@ -572,7 +575,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -626,7 +629,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -678,7 +681,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -736,7 +739,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -746,7 +749,7 @@ describe('createActivityPhaseWiring', () => { { id: 'missing-tool-reasoning', delta: { content: { type: ContentTypes.THINK, think: repeatedReasoning } }, - }, + } as never, undefined, undefined, ); @@ -766,7 +769,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -776,7 +779,7 @@ describe('createActivityPhaseWiring', () => { { id: 'current-reasoning', delta: { content: { type: ContentTypes.THINK, think: repeatedReasoning } }, - }, + } as never, undefined, undefined, ); @@ -789,7 +792,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -826,7 +829,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -858,7 +861,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -867,7 +870,7 @@ describe('createActivityPhaseWiring', () => { { id: 'reasoning-step', delta: { content: { type: ContentTypes.THINK, think: 'Compared both auth paths.' } }, - }, + } as never, undefined, undefined, ); @@ -880,7 +883,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'commentary' }, }, - }, + } as never, undefined, undefined, ); @@ -907,7 +910,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -948,7 +951,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -985,7 +988,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -994,7 +997,7 @@ describe('createActivityPhaseWiring', () => { { id: 'lane-reasoning', delta: { content: { type: ContentTypes.THINK, think: 'Checked the lane input.' } }, - }, + } as never, undefined, undefined, ); @@ -1009,7 +1012,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -1039,7 +1042,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -1083,7 +1086,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -1093,7 +1096,7 @@ describe('createActivityPhaseWiring', () => { { id, delta: { content: { type: ContentTypes.THINK, think: text } }, - }, + } as never, undefined, undefined, ); @@ -1115,7 +1118,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -1173,7 +1176,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -1254,7 +1257,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -1266,7 +1269,7 @@ describe('createActivityPhaseWiring', () => { delta: { content: { type: ContentTypes.THINK, think: 'Verified one more edge case.' }, }, - }, + } as never, undefined, undefined, ); @@ -1823,7 +1826,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -1874,7 +1877,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -1924,7 +1927,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -1979,7 +1982,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2035,7 +2038,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2090,7 +2093,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2147,7 +2150,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2216,7 +2219,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2332,7 +2335,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2408,7 +2411,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2448,7 +2451,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'think' }, }, - }, + } as never, undefined, undefined, ); @@ -2485,13 +2488,13 @@ describe('createActivityPhaseWiring', () => { message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, }; - handler?.handle(GraphEvents.ON_RUN_STEP, finalStep, undefined, undefined); + handler?.handle(GraphEvents.ON_RUN_STEP, finalStep as never, undefined, undefined); await flushDetached(); expect(generatePhase).not.toHaveBeenCalled(); handler?.handle( GraphEvents.ON_RUN_STEP, - { ...finalStep, id: 'root-final', groupId: undefined }, + { ...finalStep, id: 'root-final', groupId: undefined } as never, undefined, undefined, ); @@ -2526,13 +2529,13 @@ describe('createActivityPhaseWiring', () => { message_creation: { message_id: 'm', content_type: 'text' }, }, }; - handler?.handle(GraphEvents.ON_RUN_STEP, textStep, undefined, undefined); + handler?.handle(GraphEvents.ON_RUN_STEP, textStep as never, undefined, undefined); await flushDetached(); expect(generatePhase).not.toHaveBeenCalled(); handler?.handle( GraphEvents.ON_RUN_STEP, - { ...textStep, id: 'root-text', groupId: undefined }, + { ...textStep, id: 'root-text', groupId: undefined } as never, undefined, undefined, ); @@ -2579,7 +2582,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'commentary' }, }, - }, + } as never, undefined, undefined, ); @@ -2624,7 +2627,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2639,7 +2642,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'commentary' }, }, - }, + } as never, undefined, undefined, ); @@ -2760,7 +2763,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2778,7 +2781,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2826,7 +2829,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2880,7 +2883,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -2934,7 +2937,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -2972,7 +2975,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -3018,7 +3021,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -3027,7 +3030,7 @@ describe('createActivityPhaseWiring', () => { { id: 'boundary', delta: { content: { type: ContentTypes.TEXT, text: substantialText('Boundary result.') } }, - }, + } as never, undefined, undefined, ); @@ -3123,7 +3126,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text', phase: 'final_answer' }, }, - }, + } as never, undefined, undefined, ); @@ -3132,7 +3135,7 @@ describe('createActivityPhaseWiring', () => { { id: 'final-step', delta: { content: { type: ContentTypes.TEXT, text: 'Done.' } }, - }, + } as never, undefined, undefined, ); @@ -3194,7 +3197,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -3203,7 +3206,7 @@ describe('createActivityPhaseWiring', () => { { id: 'early-text', delta: { content: { type: ContentTypes.TEXT, text: 'Context for the earlier work.' } }, - }, + } as never, undefined, undefined, ); @@ -3305,7 +3308,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -3319,7 +3322,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -3328,7 +3331,7 @@ describe('createActivityPhaseWiring', () => { { id: 'parallel-text', delta: { content: { type: ContentTypes.TEXT, text: 'Context for the later work.' } }, - }, + } as never, undefined, undefined, ); @@ -3337,7 +3340,7 @@ describe('createActivityPhaseWiring', () => { { id: 'boundary', delta: { content: { type: ContentTypes.TEXT, text: substantialText('Boundary result.') } }, - }, + } as never, undefined, undefined, ); @@ -3402,7 +3405,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -3412,7 +3415,7 @@ describe('createActivityPhaseWiring', () => { { id: 'parallel-text', delta: { content: { type: ContentTypes.TEXT, text: 'Context for the later work.' } }, - }, + } as never, undefined, undefined, ); @@ -3421,7 +3424,7 @@ describe('createActivityPhaseWiring', () => { { id: 'boundary', delta: { content: { type: ContentTypes.TEXT, text: substantialText('Boundary result.') } }, - }, + } as never, undefined, undefined, ); @@ -3484,7 +3487,7 @@ describe('createActivityPhaseWiring', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', content_type: 'text' }, }, - }, + } as never, undefined, undefined, ); @@ -3493,7 +3496,7 @@ describe('createActivityPhaseWiring', () => { { id: 'boundary', delta: { content: { type: ContentTypes.TEXT, text: substantialText('Boundary result.') } }, - }, + } as never, undefined, undefined, ); @@ -3528,7 +3531,7 @@ describe('createAssistantPhaseStampingHandlers', () => { type: StepTypes.MESSAGE_CREATION, message_creation: { message_id: 'm', phase: 'commentary' }, }, - }, + } as never, undefined, undefined, ); @@ -3537,7 +3540,7 @@ describe('createAssistantPhaseStampingHandlers', () => { { id: 'commentary-step', delta: { content: { type: ContentTypes.TEXT, text: 'I will compare both paths.' } }, - }, + } as never, undefined, undefined, ); diff --git a/packages/api/src/agents/activityPhases/runtime.ts b/packages/api/src/agents/activityPhases/runtime.ts index c542d7c258..1d0228f828 100644 --- a/packages/api/src/agents/activityPhases/runtime.ts +++ b/packages/api/src/agents/activityPhases/runtime.ts @@ -415,13 +415,12 @@ export function createAssistantPhaseStampingHandlers( (part as { type?: string } | null)?.type === ContentTypes.TEXT ? { ...(part as Record), phase } : part; - const forwarded = { - ...(data as Record), + const forwarded = Object.assign({}, data, { delta: { ...delta, content: Array.isArray(raw) ? raw.map(stamp) : stamp(raw), }, - }; + }); return messageHandler.handle(event, forwarded, metadata, graph); }, }; diff --git a/packages/api/src/agents/hitl/resume.ts b/packages/api/src/agents/hitl/resume.ts index 36575f0c34..833ace36f5 100644 --- a/packages/api/src/agents/hitl/resume.ts +++ b/packages/api/src/agents/hitl/resume.ts @@ -324,7 +324,7 @@ type ResumableRunStep = { export function normalizeResumeRunStepIndices( runSteps: readonly T[], - seedContent: readonly { type?: string; tool_call?: { id?: string } }[] = [], + seedContent: readonly ({ type?: string; tool_call?: { id?: string } } | undefined)[] = [], ): T[] { const toolCallIndices = new Map(); seedContent.forEach((part, index) => { @@ -361,7 +361,7 @@ export function hydrateResumeRunSteps( runSteps: readonly RunStep[], stepMap: Map | undefined, graph: { toolCallStepIds?: Map } | null | undefined, - seedContent: readonly { type?: string; tool_call?: { id?: string } }[] = [], + seedContent: readonly ({ type?: string; tool_call?: { id?: string } } | undefined)[] = [], ): void { for (const runStep of normalizeResumeRunStepIndices(runSteps, seedContent)) { if (!runStep?.id) { diff --git a/packages/api/src/agents/reasoningLabels/runtime.spec.ts b/packages/api/src/agents/reasoningLabels/runtime.spec.ts index c09ddfdb1f..20aea85c12 100644 --- a/packages/api/src/agents/reasoningLabels/runtime.spec.ts +++ b/packages/api/src/agents/reasoningLabels/runtime.spec.ts @@ -91,21 +91,21 @@ function createHarness( type: StepTypes.MESSAGE_CREATION, message_creation: { content_type: ContentTypes.THINK }, }, - }, + } as never, metadata, ); }; const append = async (text: string, id = 'reasoning-1') => { await wrapped[GraphEvents.ON_REASONING_DELTA].handle( GraphEvents.ON_REASONING_DELTA, - reasoningDelta(id, text), + reasoningDelta(id, text) as never, ); }; const close = async (id = 'reasoning-1') => { await wrapped[GraphEvents.ON_RUN_STEP_CLOSED].handle(GraphEvents.ON_RUN_STEP_CLOSED, { id, status: 'completed', - }); + } as never); }; const settle = async () => { for (let i = 0; i < 5; i += 1) { diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index 341a83df6d..13b2725986 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'crypto'; import { logger } from '@librechat/data-schemas'; import { ensureHandler } from '@langchain/core/callbacks/manager'; import { Run, Providers, Constants, HookRegistry } from '@librechat/agents'; @@ -1883,6 +1884,7 @@ export async function createRun({ } const streamLimits = resolveStreamLimits(agentsEndpointConfig); + const resolvedRunId = runId ?? randomUUID(); /** * Built as a variable (not an inline literal) so the extra @@ -1892,7 +1894,7 @@ export async function createRun({ * the field at the call site once the dependency is bumped. */ const runConfig = { - runId, + runId: resolvedRunId, graphConfig, tokenCounter, customHandlers, @@ -1959,7 +1961,7 @@ export async function createRun({ // tracing is enabled. Requires @librechat/agents >= 3.2.21. langfuse: buildLangfuseConfig({ appConfig, - runId, + runId: resolvedRunId, tenantId: tenantId ?? user?.tenantId, centralTraceExportEnabled, }), diff --git a/packages/api/src/agents/steering/__tests__/offset.spec.ts b/packages/api/src/agents/steering/__tests__/offset.spec.ts index f572f99b0c..321b90086c 100644 --- a/packages/api/src/agents/steering/__tests__/offset.spec.ts +++ b/packages/api/src/agents/steering/__tests__/offset.spec.ts @@ -27,11 +27,26 @@ describe('createSteerIndexOffsetHandlers', () => { ); const handler = wrapped![GraphEvents.ON_RUN_STEP]; - handler.handle(GraphEvents.ON_RUN_STEP, { id: 'step-1', index: 0 }, undefined, undefined); + handler.handle( + GraphEvents.ON_RUN_STEP, + { id: 'step-1', index: 0 } as never, + undefined, + undefined, + ); state.offset = 1; - handler.handle(GraphEvents.ON_RUN_STEP, { id: 'step-2', index: 1 }, undefined, undefined); + handler.handle( + GraphEvents.ON_RUN_STEP, + { id: 'step-2', index: 1 } as never, + undefined, + undefined, + ); state.offset = 2; - handler.handle(GraphEvents.ON_RUN_STEP, { id: 'step-3', index: 2 }, undefined, undefined); + handler.handle( + GraphEvents.ON_RUN_STEP, + { id: 'step-3', index: 2 } as never, + undefined, + undefined, + ); expect(calls.map((c) => (c.data as { index: number }).index)).toEqual([0, 2, 4]); }); @@ -47,14 +62,14 @@ describe('createSteerIndexOffsetHandlers', () => { const handler = wrapped![GraphEvents.ON_AGENT_UPDATE]; handler.handle( GraphEvents.ON_AGENT_UPDATE, - { agent_update: { index: 3, runId: 'run-1' } }, + { agent_update: { index: 3, runId: 'run-1' } } as never, undefined, undefined, ); state.offset = 2; handler.handle( GraphEvents.ON_AGENT_UPDATE, - { agent_update: { index: 4, runId: 'run-1' } }, + { agent_update: { index: 4, runId: 'run-1' } } as never, undefined, undefined, ); @@ -79,7 +94,7 @@ describe('createSteerIndexOffsetHandlers', () => { expect(wrapped![GraphEvents.ON_MESSAGE_DELTA]).toBe(passthrough); wrapped![GraphEvents.ON_RUN_STEP].handle( GraphEvents.ON_RUN_STEP, - { id: 'step-x' }, + { id: 'step-x' } as never, undefined, undefined, ); @@ -99,9 +114,19 @@ describe('createSteerIndexOffsetHandlers', () => { ); const handler = wrapped![GraphEvents.ON_RUN_STEP]; - handler.handle(GraphEvents.ON_RUN_STEP, { id: 'step-1', index: 0 }, undefined, undefined); + handler.handle( + GraphEvents.ON_RUN_STEP, + { id: 'step-1', index: 0 } as never, + undefined, + undefined, + ); state.offset = 1; - handler.handle(GraphEvents.ON_RUN_STEP, { id: 'step-2', index: 1 }, undefined, undefined); + handler.handle( + GraphEvents.ON_RUN_STEP, + { id: 'step-2', index: 1 } as never, + undefined, + undefined, + ); // seed offset (2) applies inside; steer offset applies on top expect(calls.map((c) => (c.data as { index: number }).index)).toEqual([2, 4]); diff --git a/packages/api/src/agents/steering/__tests__/runtime.spec.ts b/packages/api/src/agents/steering/__tests__/runtime.spec.ts index f4c471cb58..f35d2908f8 100644 --- a/packages/api/src/agents/steering/__tests__/runtime.spec.ts +++ b/packages/api/src/agents/steering/__tests__/runtime.spec.ts @@ -98,7 +98,7 @@ describe('createSteerDrainHook', () => { }, }); - const output: SteerDrainOutput = await hook(batchInput(), abortSignal); + const output = (await hook(batchInput(), abortSignal)) as SteerDrainOutput; expect(applied).toEqual(['first', 'second']); expect(output.injectedMessages).toEqual([ { role: 'user', content: 'first', source: 'steer' }, @@ -156,7 +156,7 @@ describe('createSteerDrainHook', () => { }, }); - const output: SteerDrainOutput = await hook(batchInput(), abortSignal); + const output = (await hook(batchInput(), abortSignal)) as SteerDrainOutput; expect(output).toEqual({}); expect((await GenerationJobManager.steering.peek(streamId)).map((item) => item.text)).toEqual([ 'survives', @@ -194,7 +194,7 @@ describe('createSteerDrainHook', () => { buildMedia, }); - const output: SteerDrainOutput = await hook(batchInput(), abortSignal); + const output = (await hook(batchInput(), abortSignal)) as SteerDrainOutput; // buildMedia is consulted only for items that carry files. expect(buildMedia).toHaveBeenCalledTimes(1); expect(calls).toEqual(['apply:s1', 'apply:s2', 'media:s1']); @@ -227,7 +227,7 @@ describe('createSteerDrainHook', () => { }, }); - const output: SteerDrainOutput = await hook(batchInput(), abortSignal); + const output = (await hook(batchInput(), abortSignal)) as SteerDrainOutput; expect(appliedBeforeEncode).toBe(true); expect(output.injectedMessages).toEqual([ { role: 'user', content: 'must land first', source: 'steer' }, @@ -251,7 +251,7 @@ describe('createSteerDrainHook', () => { }, }); - const output: SteerDrainOutput = await hook(batchInput(), abortSignal); + const output = (await hook(batchInput(), abortSignal)) as SteerDrainOutput; expect(output.injectedMessages).toEqual([ { role: 'user', content: 'words survive', source: 'steer' }, ]); @@ -300,7 +300,7 @@ describe('createSteerPreemptBoundaryHook', () => { }, }); - const output: SteerDrainOutput = await hook(boundaryInput(), abortSignal); + const output = (await hook(boundaryInput(), abortSignal)) as SteerDrainOutput; expect(applied).toEqual(['first', 'second']); expect(output.injectedMessages).toEqual([ { role: 'user', content: 'first', source: 'steer' }, @@ -436,7 +436,7 @@ describe('createSteerPreemptBoundaryHook', () => { jobCreatedAt: job.createdAt, applySteer: jest.fn(), }); - const output: SteerDrainOutput = await hook(boundaryInput(), abortSignal); + const output = (await hook(boundaryInput(), abortSignal)) as SteerDrainOutput; expect(output.injectedMessages).toHaveLength(1); /** Both the drained id and the stale snapshot id are spent. */ @@ -485,7 +485,7 @@ describe('createSteerPreemptBoundaryHook', () => { }, }); - const output: SteerDrainOutput = await hook(boundaryInput(), abortSignal); + const output = (await hook(boundaryInput(), abortSignal)) as SteerDrainOutput; expect(output).toEqual({}); expect((await GenerationJobManager.steering.peek(streamId)).map((item) => item.text)).toEqual([ 'still injected', diff --git a/packages/api/src/agents/subagentThreads.ts b/packages/api/src/agents/subagentThreads.ts index e32854f459..40e5af74d0 100644 --- a/packages/api/src/agents/subagentThreads.ts +++ b/packages/api/src/agents/subagentThreads.ts @@ -1146,7 +1146,7 @@ export class SubagentThreadTaskStore extends InMemorySubagentTaskStore { const snapshot = this.get(scopeId, taskId); const lockKey = snapshot?.threadId == null ? undefined : `${scopeId}\u0000${snapshot.threadId}`; const lease = lockKey == null ? undefined : this.activeThreads.get(lockKey); - if (lease?.taskId === taskId && lease.settling) { + if (snapshot != null && lease?.taskId === taskId && lease.settling) { return { status: 'not_running', task: snapshot }; } const result = super.control(scopeId, taskId, command); diff --git a/packages/api/src/stream/implementations/InMemoryJobStore.ts b/packages/api/src/stream/implementations/InMemoryJobStore.ts index 2ae4ad2d18..7718af5572 100644 --- a/packages/api/src/stream/implementations/InMemoryJobStore.ts +++ b/packages/api/src/stream/implementations/InMemoryJobStore.ts @@ -31,6 +31,7 @@ import { PAUSE_PERSISTENCE_TIMEOUT_ERROR, PAUSE_PERSISTENCE_TIMEOUT_MS, isPendingActionStale, + toWireRunSteps, } from '~/stream/interfaces/IJobStore'; import { isRecoveredSteerPayload, @@ -1466,7 +1467,7 @@ export class InMemoryJobStore implements IJobStoreV2 { // Dereference WeakRef - may return undefined if GC'd const graph = state.graphRef.deref(); - return graph?.contentData ?? []; + return toWireRunSteps(graph?.contentData ?? []); } /** diff --git a/packages/api/src/stream/implementations/RedisJobStore.ts b/packages/api/src/stream/implementations/RedisJobStore.ts index fd794c5854..56addef346 100644 --- a/packages/api/src/stream/implementations/RedisJobStore.ts +++ b/packages/api/src/stream/implementations/RedisJobStore.ts @@ -34,6 +34,7 @@ import { PAUSE_PERSISTENCE_TIMEOUT_ERROR, PAUSE_PERSISTENCE_TIMEOUT_MS, isPendingActionStale, + toWireRunSteps, } from '~/stream/interfaces/IJobStore'; import { MAX_COALESCED_BYTES, @@ -3661,7 +3662,7 @@ export class RedisJobStore implements IJobStoreV2 { g.getRunSteps(), ); if (localSteps && localSteps.length > 0) { - return localSteps; + return toWireRunSteps(localSteps); } } // Note: Don't delete from cache here - graph may still be valid diff --git a/packages/api/src/stream/interfaces/IJobStore.ts b/packages/api/src/stream/interfaces/IJobStore.ts index 8e469d0857..ed1d5f44ca 100644 --- a/packages/api/src/stream/interfaces/IJobStore.ts +++ b/packages/api/src/stream/interfaces/IJobStore.ts @@ -4,12 +4,63 @@ import type { TPendingSteer, UserSubmittedMessageFieldPath, } from 'librechat-data-provider'; -import type { StandardGraph } from '@librechat/agents'; +import type { RunStep, StandardGraph } from '@librechat/agents'; import type { ActivityPhaseSnapshot } from '~/agents/activityPhases/runtime'; import type { ResolvedAskUserQuestion } from '~/agents/hitl/resume'; import type { RecoveredSteerPayload } from '../SteerRecovery'; import type { MCPRuntimeRequestBody } from '~/mcp/types'; +/** + * Rewrites string-enum members to their literal values, recursively. The SDK and + * data-provider declare nominally distinct enums (`ContentTypes`, `StepTypes`, ...) + * with identical string values; erasing that nominality is what lets the two run-step + * contracts be compared structurally. + */ +type WireShape = T extends string + ? `${T}` + : T extends readonly (infer U)[] + ? WireShape[] + : T extends object + ? { [K in keyof T]: WireShape } + : T; + +type StaticAssert = T; + +/** + * Compile-time proof that the SDK run step and the wire contract (`Agents.RunStep`) + * agree structurally once enum nominality is erased: any added, removed, retyped, or + * newly optional SDK field fails these assertions, so drift cannot silently enter + * resume state through `toWireRunSteps`. + * + * `summary.content` is the one deliberately unchecked field: the SDK reuses its full + * `MessageContentComplex` union there, while the wire contract narrows it to the plain + * text blocks summarization actually emits. That narrowing is the single semantic + * judgment this conversion vouches for. + */ +type _WireRunStepContractHolds = StaticAssert< + WireShape> extends WireShape> + ? true + : false +>; + +type _WireSummaryContractHolds = StaticAssert< + WireShape, 'content'>> extends WireShape< + Omit, 'content'> + > + ? true + : false +>; + +/** + * Run steps living on the SDK graph serialize to exactly the wire shape + * `Agents.RunStep` describes; the assertion is safe because + * `_WireRunStepContractHolds` above proves the contracts identical modulo the + * nominally-split enums, which share their string values at runtime. + */ +export function toWireRunSteps(steps: readonly RunStep[]): Agents.RunStep[] { + return steps as Agents.RunStep[]; +} + /** * A pause owner has this long to durably persist the interrupted turn before * the barrier is considered abandoned. An abandoned barrier must fail closed: diff --git a/packages/api/src/types/agents.d.ts b/packages/api/src/types/agents.d.ts deleted file mode 100644 index 26c14e012c..0000000000 --- a/packages/api/src/types/agents.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * `@librechat/agents` publishes its declaration files with its internal `@/*` path aliases - * unrewritten, so a consumer cannot resolve them. `types/llm.d.ts` imports `Providers` that - * way, which leaves `ProviderOptionsMap`'s computed keys unresolved and collapses - * `keyof ProviderOptionsMap` to `number`. Until v3.6.16 that only degraded `LLMConfig` - * silently; v3.6.16 made `SharedLLMConfig` generic over that key union, so `provider` became - * `number | RuntimeProviderName` and no real provider was assignable to it. - * - * Declaring the one alias `llm.d.ts` needs restores the enum, and with it the provider key - * union. Remove this once the SDK ships declarations with its aliases resolved — note that - * mapping every `@/*` alias instead unmasks a large backlog of latent errors elsewhere in - * this package, so widening it is a separate cleanup rather than a drop-in improvement. - */ -declare module '@/common' { - export { Providers } from '@librechat/agents'; -} diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index f3e4a5bb0a..10246b7aa5 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -19,7 +19,11 @@ "noEmit": true, "sourceMap": true, "paths": { - "~/*": ["./src/*"] + "~/*": ["./src/*"], + // @librechat/agents ships declaration files that import its internal "@/" alias, which + // package exports cannot map; resolve them into its published types until the SDK + // rewrites the aliases at build time. + "@/*": ["../../node_modules/@librechat/agents/dist/types/*"] } }, "ts-node": { diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index c24f1dfeec..39d62751b4 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -1,10 +1,5 @@ import { z } from 'zod'; -import type { - TMessageContentParts, - AgentSubagentGraph, - FunctionToolCall, - FunctionTool, -} from './types/assistants'; +import type { TMessageContentParts, AgentSubagentGraph, FunctionTool } from './types/assistants'; import type { SearchResultData } from './types/web'; import type { TFile } from './types/files'; import { userSubmittedMessageFieldPathSchema } from './filters'; @@ -391,7 +386,9 @@ export const ImageVisionTool: FunctionTool = { }, }; -export const isImageVisionTool = (tool: FunctionTool | FunctionToolCall) => +/** Structural on purpose: accepts assistants tools/tool calls and agents function tool + * calls alike — the check only ever reads `type` and `function.name`. */ +export const isImageVisionTool = (tool: { type?: string; function?: { name?: string } }) => tool.type === 'function' && tool.function?.name === ImageVisionTool.function?.name; export const openAISettings = { diff --git a/packages/data-provider/src/types/agents.ts b/packages/data-provider/src/types/agents.ts index 35d2e39c9f..a51d2870d9 100644 --- a/packages/data-provider/src/types/agents.ts +++ b/packages/data-provider/src/types/agents.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ import type { TTokenUsageEvent, TContextUsageEvent, TPendingSteer } from './runs'; -import type { FunctionToolCall, SummaryContentPart } from './assistants'; import type { TAttachment, TPlugin } from 'src/schemas'; +import type { SummaryContentPart } from './assistants'; import { StepTypes, ContentTypes, ToolCallTypes } from './runs'; export namespace Agents { @@ -67,8 +67,9 @@ export namespace Agents { * A call to a tool. */ export type ToolCall = { - /** Type ("tool_call") according to Assistants Tool Call Structure */ - type: ToolCallTypes.TOOL_CALL; + /** Type ("tool_call") according to Assistants Tool Call Structure; optional literal + * form included to mirror langchain's ToolCall, whose `type` is optional. */ + type?: ToolCallTypes.TOOL_CALL | 'tool_call'; /** The name of the tool to be called */ name: string; @@ -203,7 +204,8 @@ export namespace Agents { groupId?: number; // #new stepDetails: StepDetails; summary?: SummaryContentPart; - usage: null | object; + /** Optional to mirror the agents SDK, which omits usage until a step reports it. */ + usage?: null | object; /** Epoch ms the step was opened. Emitted by `@librechat/agents` >= 3.4.6. */ created_at?: number; status?: RunStepStatus; @@ -324,7 +326,7 @@ export namespace Agents { }; export type ToolCallsDetails = { type: StepTypes.TOOL_CALLS; - tool_calls: AgentToolCall[]; + tool_calls?: AgentToolCall[]; }; export type ToolCallDelta = { type: StepTypes.TOOL_CALLS | string; @@ -338,7 +340,22 @@ export namespace Agents { description?: string; }; }; - export type AgentToolCall = FunctionToolCall | ToolCall; + /** + * Mirrors the agents SDK's function tool-call variant: `arguments` may arrive as a + * parsed object, and `output` is attached by LibreChat aggregation only once available + * (the legacy assistants `FunctionToolCall` requires both as string/present). + */ + export type AgentFunctionToolCall = { + id: string; + type: 'function'; + function: { + name: string; + arguments: string | object; + output?: string | null; + }; + }; + + export type AgentToolCall = AgentFunctionToolCall | ToolCall; /** * Human-in-the-loop interrupt categories. The discriminator on @@ -734,8 +751,20 @@ export type GraphEdge = { * * For handoff edges: Description for the input parameter that the handoff tool accepts, * allowing the supervisor to pass specific instructions/context to the transferred agent. + * + * The callback receives a minimal structural view of the run's messages (data-provider + * cannot depend on langchain's BaseMessage): every langchain message satisfies + * `{ content: unknown }`, and callbacks typed against richer structural message shapes + * remain assignable. The promise branch mirrors the agents SDK signature exactly — + * widening it (e.g. to `Promise`) would break assignability of + * stored edges into the SDK's `GraphEdge`. */ - prompt?: string | ((messages: BaseMessage[], runStartIndex: number) => string | undefined); + prompt?: + | string + | (( + messages: { content: unknown }[], + runStartIndex: number, + ) => string | Promise | undefined); /** * When true, excludes messages from startIndex when adding prompt. * Automatically set to true when {results} variable is used in prompt.