@librechat/agents 3.6.0 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/run.ts CHANGED
@@ -16,8 +16,11 @@ import {
16
16
  HumanMessage,
17
17
  SystemMessage,
18
18
  } from '@langchain/core/messages';
19
+ import type {
20
+ MessageContentComplex,
21
+ UsageMetadata,
22
+ } from '@langchain/core/messages';
19
23
  import type { StringPromptValue } from '@langchain/core/prompt_values';
20
- import type { MessageContentComplex } from '@langchain/core/messages';
21
24
  import type { RunnableConfig } from '@langchain/core/runnables';
22
25
  import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
23
26
  import type { StandardGraph } from '@/graphs/Graph';
@@ -29,10 +32,6 @@ import {
29
32
  SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
30
33
  SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
31
34
  } from '@/tools/subagent/SubagentReplay';
32
- import {
33
- getRunStepResumeState,
34
- stripRunStepResumeState,
35
- } from '@/tools/runStepResume';
36
35
  import {
37
36
  ACTIVITY_PHASE_LABEL_PROMPT,
38
37
  ACTIVITY_LABEL_PROMPT,
@@ -48,6 +47,12 @@ import {
48
47
  isLangfuseCallbackHandler,
49
48
  withLangfuseAttributes,
50
49
  } from '@/langfuse';
50
+ import {
51
+ REASONING_LABEL_PROMPT,
52
+ buildReasoningLabelTraceSeed,
53
+ buildReasoningLabelPrompt,
54
+ normalizeReasoningLabel,
55
+ } from '@/prompts/reasoningLabel';
51
56
  import {
52
57
  hasToolOutputTracingConfig,
53
58
  resolveLangfuseConfig,
@@ -63,6 +68,10 @@ import {
63
68
  resolveLangfuseRuntimeScope,
64
69
  withLangfuseRuntimeScope,
65
70
  } from '@/langfuseRuntimeScope';
71
+ import {
72
+ getRunStepResumeState,
73
+ stripRunStepResumeState,
74
+ } from '@/tools/runStepResume';
66
75
  import {
67
76
  Callback,
68
77
  GraphEvents,
@@ -100,6 +109,7 @@ export const defaultOmitOptions = new Set([
100
109
 
101
110
  const ACTIVITY_LABEL_TRACE_NAME = 'LibreChat Activity Label';
102
111
  const ACTIVITY_PHASE_TRACE_NAME = 'LibreChat Activity Phase';
112
+ const REASONING_LABEL_TRACE_NAME = 'LibreChat Reasoning Label';
103
113
 
104
114
  const CUSTOM_GRAPH_EVENTS = new Set<string>([
105
115
  GraphEvents.ON_AGENT_UPDATE,
@@ -312,6 +322,8 @@ export class Run<_T extends t.BaseGraphState> {
312
322
  private activityLabelSeq = 0;
313
323
  /** Per-run sequence for parent activity-phase trace and invocation ids. */
314
324
  private activityPhaseLabelSeq = 0;
325
+ /** Per-run sequence for reasoning-label trace and invocation ids. */
326
+ private reasoningLabelSeq = 0;
315
327
  /** Latest user turn used to keep detached phase roots conversation-shaped. */
316
328
  private activityPhaseTraceInput?: string;
317
329
  /** Distinguishes sibling forks started from the same explicit checkpoint. */
@@ -2354,6 +2366,261 @@ export class Run<_T extends t.BaseGraphState> {
2354
2366
  }
2355
2367
  }
2356
2368
 
2369
+ /**
2370
+ * Generates one replacement title for a user-visible reasoning step. Hosts
2371
+ * own accumulation, scheduling, revision ordering, durable delivery, and
2372
+ * billing; the SDK only performs one bounded, redaction-aware generation.
2373
+ */
2374
+ async generateReasoningLabel({
2375
+ provider,
2376
+ clientOptions,
2377
+ visibleReasoning,
2378
+ reasoningStepId,
2379
+ revision,
2380
+ status = 'streaming',
2381
+ previousLabel,
2382
+ agentId,
2383
+ prompt,
2384
+ charLimit = 6_000,
2385
+ chainOptions,
2386
+ traceSeed,
2387
+ sourceRunId,
2388
+ sourceTraceId,
2389
+ responseId,
2390
+ }: t.RunReasoningLabelOptions): Promise<t.ReasoningLabelResult> {
2391
+ const normalizedStepId = reasoningStepId.trim();
2392
+ const snapshotChars = visibleReasoning.trim().length;
2393
+ if (
2394
+ normalizedStepId === '' ||
2395
+ snapshotChars === 0 ||
2396
+ !Number.isInteger(revision) ||
2397
+ revision < 0
2398
+ ) {
2399
+ return {};
2400
+ }
2401
+
2402
+ const reasoningSeq = ++this.reasoningLabelSeq;
2403
+ const requestedContext =
2404
+ this.Graph == null || agentId == null
2405
+ ? undefined
2406
+ : this.Graph.agentContexts.get(agentId);
2407
+ if (agentId != null && requestedContext == null) {
2408
+ return {};
2409
+ }
2410
+ if (agentId == null && (this.Graph?.agentContexts.size ?? 0) > 1) {
2411
+ return {};
2412
+ }
2413
+ const reasoningContext =
2414
+ this.Graph == null
2415
+ ? undefined
2416
+ : (requestedContext ??
2417
+ this.Graph.agentContexts.get(this.Graph.defaultAgentId));
2418
+ const reasoningChainOptions = {
2419
+ ...(chainOptions ?? {}),
2420
+ } as Partial<RunnableConfig> & {
2421
+ configurable?: Record<string, unknown> & {
2422
+ requestBody?: { parentMessageId?: unknown };
2423
+ };
2424
+ };
2425
+ const reasoningUserId =
2426
+ typeof reasoningChainOptions.configurable?.user_id === 'string'
2427
+ ? reasoningChainOptions.configurable.user_id
2428
+ : undefined;
2429
+ const reasoningSessionId =
2430
+ typeof reasoningChainOptions.configurable?.thread_id === 'string'
2431
+ ? reasoningChainOptions.configurable.thread_id
2432
+ : undefined;
2433
+ const reasoningParentMessageId =
2434
+ reasoningChainOptions.configurable?.requestBody?.parentMessageId;
2435
+ const reasoningAgentId =
2436
+ agentId ??
2437
+ (this.Graph?.agentContexts.size === 1
2438
+ ? this.Graph.defaultAgentId
2439
+ : undefined);
2440
+ const reasoningAgentName =
2441
+ reasoningAgentId == null ? undefined : reasoningContext?.name;
2442
+ const resolvedSourceRunId = sourceRunId ?? this.id;
2443
+ const reasoningResponseId = responseId ?? this.id;
2444
+ const reasoningMetadata: Record<string, unknown> = {
2445
+ sourceRunId: resolvedSourceRunId,
2446
+ ...(sourceTraceId == null ? {} : { sourceTraceId }),
2447
+ responseId: reasoningResponseId,
2448
+ reasoningStepId: normalizedStepId,
2449
+ revision,
2450
+ status,
2451
+ snapshotChars,
2452
+ ...(typeof reasoningParentMessageId === 'string'
2453
+ ? { parentMessageId: reasoningParentMessageId }
2454
+ : {}),
2455
+ ...(reasoningAgentId == null ? {} : { agentId: reasoningAgentId }),
2456
+ ...(reasoningAgentName == null ? {} : { agentName: reasoningAgentName }),
2457
+ };
2458
+ const traceMetadata = {
2459
+ ...createLangfuseTraceMetadata({
2460
+ messageId: `reasoning-label-${reasoningResponseId}`,
2461
+ parentMessageId: reasoningParentMessageId,
2462
+ agentId: reasoningAgentId,
2463
+ agentName: reasoningAgentName,
2464
+ }),
2465
+ sourceRunId: resolvedSourceRunId,
2466
+ ...(sourceTraceId == null ? {} : { sourceTraceId }),
2467
+ responseId: reasoningResponseId,
2468
+ reasoningStepId: normalizedStepId,
2469
+ revision: String(revision),
2470
+ status,
2471
+ snapshotChars: String(snapshotChars),
2472
+ };
2473
+ const reasoningRunName =
2474
+ reasoningChainOptions.runName ?? REASONING_LABEL_TRACE_NAME;
2475
+ const reasoningTags = ['librechat', 'reasoning-label', 'reasoning-step'];
2476
+ const reasoningLangfuseConfig = resolveLangfuseConfig(
2477
+ this.langfuse,
2478
+ reasoningContext?.langfuse
2479
+ );
2480
+ initializeLangfuseTracing(reasoningLangfuseConfig);
2481
+
2482
+ const inheritedTraceSeed = getTraceIdSeed();
2483
+ const reasoningTraceSeed =
2484
+ reasoningLangfuseConfig?.deterministicTraceId === true ||
2485
+ inheritedTraceSeed != null
2486
+ ? (traceSeed ??
2487
+ buildReasoningLabelTraceSeed(
2488
+ resolvedSourceRunId,
2489
+ normalizedStepId,
2490
+ revision
2491
+ ))
2492
+ : undefined;
2493
+ const reasoningScopeRunId = `reasoning-label:${this.id}:${reasoningSeq}:${nanoid()}`;
2494
+ const reasoningRuntimeScope = resolveLangfuseRuntimeScope({
2495
+ runLangfuse: this.langfuse,
2496
+ langfuseOverlay: reasoningContext?.langfuse,
2497
+ traceIdSeed: reasoningTraceSeed,
2498
+ runId: reasoningScopeRunId,
2499
+ });
2500
+ let reasoningLangfuseHandler: CallbackEntry | undefined;
2501
+ if (reasoningSessionId != null) {
2502
+ reasoningLangfuseHandler = createLangfuseHandler({
2503
+ langfuse: reasoningLangfuseConfig,
2504
+ userId: reasoningUserId,
2505
+ sessionId: reasoningSessionId,
2506
+ traceMetadata,
2507
+ tags: reasoningTags,
2508
+ traceIdSeed:
2509
+ reasoningLangfuseConfig?.deterministicTraceId === true
2510
+ ? reasoningTraceSeed
2511
+ : undefined,
2512
+ runId: reasoningScopeRunId,
2513
+ toolOutputTracing: reasoningRuntimeScope.toolOutputTracing,
2514
+ traceName: reasoningRunName,
2515
+ });
2516
+ }
2517
+ if (reasoningLangfuseHandler != null) {
2518
+ reasoningChainOptions.callbacks = appendCallbacks(
2519
+ reasoningChainOptions.callbacks,
2520
+ [reasoningLangfuseHandler]
2521
+ );
2522
+ }
2523
+
2524
+ const redaction = hasToolOutputTracingConfig(
2525
+ this.langfuse,
2526
+ reasoningContext?.langfuse
2527
+ )
2528
+ ? resolveToolOutputTracingConfig(
2529
+ this.langfuse,
2530
+ reasoningContext?.langfuse
2531
+ )
2532
+ : undefined;
2533
+ const userPrompt = buildReasoningLabelPrompt({
2534
+ visibleReasoning,
2535
+ status,
2536
+ charLimit,
2537
+ previousLabel,
2538
+ redaction,
2539
+ });
2540
+ if (userPrompt === '') {
2541
+ await disposeLangfuseHandler(reasoningLangfuseHandler);
2542
+ return {};
2543
+ }
2544
+
2545
+ const model = initializeModel({
2546
+ provider,
2547
+ clientOptions: {
2548
+ ...(clientOptions ?? {}),
2549
+ streaming: false,
2550
+ } as t.ClientOptions,
2551
+ }) as t.ChatModelInstance;
2552
+ const reasoningRunId = `${this.id}-reasoning-${reasoningSeq}`;
2553
+ const invokeConfig = Object.assign({}, reasoningChainOptions, {
2554
+ run_id: reasoningRunId,
2555
+ runId: reasoningRunId,
2556
+ runName: reasoningRunName,
2557
+ tags: [
2558
+ ...new Set([...(reasoningChainOptions.tags ?? []), ...reasoningTags]),
2559
+ ],
2560
+ metadata: {
2561
+ ...(reasoningChainOptions.metadata ?? {}),
2562
+ ...reasoningMetadata,
2563
+ },
2564
+ }) as Partial<RunnableConfig>;
2565
+ const invokeLabel = (
2566
+ runtimeConfig: Partial<RunnableConfig>
2567
+ ): Promise<unknown> =>
2568
+ withLangfuseAttributes(
2569
+ {
2570
+ langfuse: reasoningLangfuseConfig,
2571
+ userId: reasoningUserId,
2572
+ sessionId: reasoningSessionId,
2573
+ traceName: reasoningRunName,
2574
+ traceMetadata,
2575
+ tags: reasoningTags,
2576
+ },
2577
+ () =>
2578
+ model.invoke(
2579
+ [
2580
+ new SystemMessage(prompt ?? REASONING_LABEL_PROMPT),
2581
+ new HumanMessage(userPrompt),
2582
+ ],
2583
+ runtimeConfig
2584
+ )
2585
+ );
2586
+ const extractResult = (response: unknown): t.ReasoningLabelResult => {
2587
+ const result = response as {
2588
+ content?: unknown;
2589
+ usage_metadata?: UsageMetadata;
2590
+ } | null;
2591
+ const content = result?.content;
2592
+ let text = '';
2593
+ if (typeof content === 'string') {
2594
+ text = content;
2595
+ } else if (Array.isArray(content)) {
2596
+ text = content
2597
+ .map((block) =>
2598
+ typeof block === 'string'
2599
+ ? block
2600
+ : ((block as { text?: string }).text ?? '')
2601
+ )
2602
+ .join('');
2603
+ }
2604
+ const label = normalizeReasoningLabel(text);
2605
+ return {
2606
+ ...(label === '' ? {} : { label }),
2607
+ ...(result?.usage_metadata == null
2608
+ ? {}
2609
+ : { usage: result.usage_metadata }),
2610
+ };
2611
+ };
2612
+
2613
+ try {
2614
+ const response = await withLangfuseRuntimeScope(
2615
+ reasoningRuntimeScope,
2616
+ () => invokeLabel(invokeConfig)
2617
+ );
2618
+ return extractResult(response);
2619
+ } finally {
2620
+ await disposeLangfuseHandler(reasoningLangfuseHandler);
2621
+ }
2622
+ }
2623
+
2357
2624
  /**
2358
2625
  * Generates one parent summary for two or more logical activities. The
2359
2626
  * summary model is traced as a dedicated activity-phase chain root in the
@@ -10,3 +10,4 @@ export * from './tools';
10
10
  export * from './summarize';
11
11
  export * from './activityLabel';
12
12
  export * from './assistantPhase';
13
+ export * from './reasoningLabel';
@@ -0,0 +1,59 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type { UsageMetadata } from '@langchain/core/messages';
3
+ import type { ClientOptions } from '@/types/llm';
4
+ import type { Providers } from '@/common';
5
+
6
+ /** Lifecycle state of the visible reasoning snapshot being labeled. */
7
+ export type ReasoningLabelStatus = 'streaming' | 'complete';
8
+
9
+ /** Result of one reasoning-label revision. */
10
+ export type ReasoningLabelResult = {
11
+ label?: string;
12
+ /** Provider-reported usage for host-side billing after a durable commit. */
13
+ usage?: UsageMetadata;
14
+ };
15
+
16
+ /** Options for `Run.generateReasoningLabel`. */
17
+ export type RunReasoningLabelOptions = {
18
+ provider: Providers;
19
+ clientOptions?: ClientOptions;
20
+ /**
21
+ * Complete user-visible reasoning accumulated for this step so far. Hidden
22
+ * chain-of-thought must never be supplied through this API.
23
+ */
24
+ visibleReasoning: string;
25
+ /** Stable run-step identity shared by every revision of this label. */
26
+ reasoningStepId: string;
27
+ /** Monotonically increasing host revision for this reasoning step. */
28
+ revision: number;
29
+ /** Whether the snapshot can still grow. Default `streaming`. */
30
+ status?: ReasoningLabelStatus;
31
+ /**
32
+ * Last durably visible label for this step. The model repeats it exactly
33
+ * when the reasoning direction has not materially changed, allowing hosts
34
+ * to avoid redundant UI updates.
35
+ */
36
+ previousLabel?: string;
37
+ /**
38
+ * Agent that emitted the reasoning. Selects its Langfuse overlay and
39
+ * redaction policy. Unknown agents and omitted multi-agent ownership fail
40
+ * closed before tracing or generation.
41
+ */
42
+ agentId?: string;
43
+ /** Override for the default reasoning-label system prompt. */
44
+ prompt?: string;
45
+ /** Maximum reasoning characters retained in the prompt. Default 6000. */
46
+ charLimit?: number;
47
+ /** LangChain runnable config carrier (signal, callbacks, thread/user ids). */
48
+ chainOptions?: Partial<RunnableConfig> & {
49
+ configurable?: Record<string, unknown>;
50
+ };
51
+ /** Deterministic seed for this reasoning-label revision trace. */
52
+ traceSeed?: string;
53
+ /** Stable source run identifier recorded on the observation. */
54
+ sourceRunId?: string;
55
+ /** Source Langfuse trace id for linking this detached label trace. */
56
+ sourceTraceId?: string;
57
+ /** Host response/message identifier recorded on the observation. */
58
+ responseId?: string;
59
+ };