@librechat/agents 3.4.3 → 3.4.4

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.
Files changed (75) hide show
  1. package/dist/cjs/graphs/Graph.cjs +27 -14
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  4. package/dist/cjs/instrumentation.cjs +3 -3
  5. package/dist/cjs/langfuse.cjs +3 -3
  6. package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
  7. package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
  8. package/dist/cjs/main.cjs +5 -1
  9. package/dist/cjs/messages/assistantPhase.cjs +59 -0
  10. package/dist/cjs/messages/assistantPhase.cjs.map +1 -0
  11. package/dist/cjs/messages/index.cjs +1 -0
  12. package/dist/cjs/prompts/activityLabel.cjs +76 -0
  13. package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
  14. package/dist/cjs/run.cjs +200 -10
  15. package/dist/cjs/run.cjs.map +1 -1
  16. package/dist/cjs/session/AgentSession.cjs +1 -1
  17. package/dist/cjs/stream.cjs +45 -8
  18. package/dist/cjs/stream.cjs.map +1 -1
  19. package/dist/cjs/tools/ToolNode.cjs +3 -3
  20. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +81 -6
  21. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  22. package/dist/cjs/utils/callbacks.cjs +8 -0
  23. package/dist/cjs/utils/callbacks.cjs.map +1 -1
  24. package/dist/esm/graphs/Graph.mjs +27 -14
  25. package/dist/esm/graphs/Graph.mjs.map +1 -1
  26. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  27. package/dist/esm/instrumentation.mjs +3 -3
  28. package/dist/esm/langfuse.mjs +3 -3
  29. package/dist/esm/langfuseRuntimeScope.mjs +1 -1
  30. package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
  31. package/dist/esm/main.mjs +3 -2
  32. package/dist/esm/messages/assistantPhase.mjs +57 -0
  33. package/dist/esm/messages/assistantPhase.mjs.map +1 -0
  34. package/dist/esm/messages/index.mjs +1 -0
  35. package/dist/esm/prompts/activityLabel.mjs +74 -1
  36. package/dist/esm/prompts/activityLabel.mjs.map +1 -1
  37. package/dist/esm/run.mjs +202 -12
  38. package/dist/esm/run.mjs.map +1 -1
  39. package/dist/esm/session/AgentSession.mjs +1 -1
  40. package/dist/esm/stream.mjs +45 -8
  41. package/dist/esm/stream.mjs.map +1 -1
  42. package/dist/esm/tools/ToolNode.mjs +3 -3
  43. package/dist/esm/tools/subagent/SubagentExecutor.mjs +81 -6
  44. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  45. package/dist/esm/utils/callbacks.mjs +8 -1
  46. package/dist/esm/utils/callbacks.mjs.map +1 -1
  47. package/dist/types/messages/assistantPhase.d.ts +22 -0
  48. package/dist/types/messages/index.d.ts +1 -0
  49. package/dist/types/prompts/activityLabel.d.ts +21 -1
  50. package/dist/types/run.d.ts +15 -2
  51. package/dist/types/types/activityLabel.d.ts +63 -0
  52. package/dist/types/types/assistantPhase.d.ts +6 -0
  53. package/dist/types/types/graph.d.ts +8 -1
  54. package/dist/types/types/index.d.ts +1 -0
  55. package/dist/types/types/stream.d.ts +11 -0
  56. package/dist/types/utils/callbacks.d.ts +1 -0
  57. package/package.json +1 -1
  58. package/src/graphs/Graph.ts +33 -9
  59. package/src/graphs/__tests__/Graph.reasoning.test.ts +57 -0
  60. package/src/messages/assistantPhase.test.ts +75 -0
  61. package/src/messages/assistantPhase.ts +91 -0
  62. package/src/messages/index.ts +1 -0
  63. package/src/prompts/activityLabel.ts +177 -1
  64. package/src/run.ts +403 -21
  65. package/src/specs/activity-label-prompt.test.ts +123 -1
  66. package/src/specs/activity-phase-label.test.ts +306 -0
  67. package/src/stream.ts +69 -12
  68. package/src/tools/__tests__/SubagentExecutor.test.ts +436 -0
  69. package/src/tools/subagent/SubagentExecutor.ts +160 -8
  70. package/src/types/activityLabel.ts +65 -0
  71. package/src/types/assistantPhase.ts +6 -0
  72. package/src/types/graph.ts +8 -0
  73. package/src/types/index.ts +1 -0
  74. package/src/types/stream.ts +9 -0
  75. package/src/utils/callbacks.ts +21 -0
package/src/run.ts CHANGED
@@ -4,17 +4,18 @@ import { PromptTemplate } from '@langchain/core/prompts';
4
4
  import { RunnableLambda } from '@langchain/core/runnables';
5
5
  import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
6
6
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
7
- import {
8
- BaseMessage,
9
- HumanMessage,
10
- SystemMessage,
11
- } from '@langchain/core/messages';
12
7
  import {
13
8
  Command,
14
9
  INTERRUPT,
15
10
  MemorySaver,
16
11
  isInterrupted,
17
12
  } from '@langchain/langgraph';
13
+ import {
14
+ AIMessage,
15
+ BaseMessage,
16
+ HumanMessage,
17
+ SystemMessage,
18
+ } from '@langchain/core/messages';
18
19
  import type { StringPromptValue } from '@langchain/core/prompt_values';
19
20
  import type { MessageContentComplex } from '@langchain/core/messages';
20
21
  import type { RunnableConfig } from '@langchain/core/runnables';
@@ -28,6 +29,13 @@ import {
28
29
  SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
29
30
  SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
30
31
  } from '@/tools/subagent/SubagentReplay';
32
+ import {
33
+ ACTIVITY_PHASE_LABEL_PROMPT,
34
+ ACTIVITY_LABEL_PROMPT,
35
+ buildActivityLabelPrompt,
36
+ buildActivityPhaseLabelPrompt,
37
+ normalizeActivityPhaseLabel,
38
+ } from '@/prompts/activityLabel';
31
39
  import {
32
40
  createLangfuseTraceMetadata,
33
41
  createLangfuseHandler,
@@ -45,10 +53,6 @@ import {
45
53
  resolveLangfuseRuntimeScope,
46
54
  withLangfuseRuntimeScope,
47
55
  } from '@/langfuseRuntimeScope';
48
- import {
49
- ACTIVITY_LABEL_PROMPT,
50
- buildActivityLabelPrompt,
51
- } from '@/prompts/activityLabel';
52
56
  import {
53
57
  Callback,
54
58
  GraphEvents,
@@ -57,6 +61,7 @@ import {
57
61
  } from '@/common';
58
62
  import {
59
63
  appendCallbacks,
64
+ filterCallbacks,
60
65
  findCallback,
61
66
  type CallbackEntry,
62
67
  } from '@/utils/callbacks';
@@ -246,6 +251,10 @@ export class Run<_T extends t.BaseGraphState> {
246
251
  private _interrupt: t.RunInterruptResult<unknown> | undefined;
247
252
  /** Per-run sequence for batch-unique activity-label trace-seed fallbacks. */
248
253
  private activityLabelSeq = 0;
254
+ /** Per-run sequence for parent activity-phase trace and invocation ids. */
255
+ private activityPhaseLabelSeq = 0;
256
+ /** Latest user turn used to keep detached phase roots conversation-shaped. */
257
+ private activityPhaseTraceInput?: string;
249
258
  /** Distinguishes sibling forks started from the same explicit checkpoint. */
250
259
  private checkpointForkSeq = 0;
251
260
  private _haltedReason: string | undefined;
@@ -826,6 +835,11 @@ export class Run<_T extends t.BaseGraphState> {
826
835
  */
827
836
  const isResume = inputs instanceof Command;
828
837
  const stateInputs = isResume ? undefined : (inputs as t.IState);
838
+ if (stateInputs != null) {
839
+ this.activityPhaseTraceInput = findActivityPhaseTraceInput(
840
+ stateInputs.messages
841
+ );
842
+ }
829
843
 
830
844
  /**
831
845
  * Every honored seal costs one extra superstep, so a preemption-enabled
@@ -1565,9 +1579,12 @@ export class Run<_T extends t.BaseGraphState> {
1565
1579
  }
1566
1580
  const persistedMessages = getPersistedMessages(snapshot);
1567
1581
  if (persistedMessages != null) {
1568
- this.Graph?.restoreCheckpointMessages(
1569
- persistedMessages,
1570
- getResumeUpdateMessages(resumeUpdate)
1582
+ const resumeMessages = getResumeUpdateMessages(resumeUpdate);
1583
+ this.Graph?.restoreCheckpointMessages(persistedMessages, resumeMessages);
1584
+ this.activityPhaseTraceInput = findActivityPhaseTraceInput(
1585
+ resumeMessages == null
1586
+ ? persistedMessages
1587
+ : [...persistedMessages, ...resumeMessages]
1571
1588
  );
1572
1589
  }
1573
1590
 
@@ -1815,6 +1832,7 @@ export class Run<_T extends t.BaseGraphState> {
1815
1832
  entries,
1816
1833
  thinkingExcerpts,
1817
1834
  lastAssistantText,
1835
+ lastAssistantPhase,
1818
1836
  previousLabels,
1819
1837
  prompt,
1820
1838
  charLimit = 600,
@@ -1988,7 +2006,8 @@ export class Run<_T extends t.BaseGraphState> {
1988
2006
  entries,
1989
2007
  charLimit,
1990
2008
  thinkingExcerpts,
1991
- lastAssistantText,
2009
+ lastAssistantText:
2010
+ lastAssistantPhase === 'final_answer' ? undefined : lastAssistantText,
1992
2011
  previousLabels,
1993
2012
  redaction,
1994
2013
  });
@@ -2085,9 +2104,12 @@ export class Run<_T extends t.BaseGraphState> {
2085
2104
  invokeConfig.callbacks,
2086
2105
  isLangfuseCallbackHandler
2087
2106
  );
2088
- const { callbacks: _cb, ...rest } = invokeConfig;
2107
+ const { callbacks, ...rest } = invokeConfig;
2089
2108
  const safeConfig = Object.assign({}, rest, {
2090
- callbacks: langfuseHandler ? [langfuseHandler] : [],
2109
+ callbacks: filterCallbacks(
2110
+ callbacks,
2111
+ (callback) => callback === langfuseHandler
2112
+ ),
2091
2113
  });
2092
2114
  response = await withLangfuseRuntimeScope(labelRuntimeScope, () =>
2093
2115
  invokeLabel(safeConfig as Partial<RunnableConfig>)
@@ -2099,6 +2121,347 @@ export class Run<_T extends t.BaseGraphState> {
2099
2121
  await disposeLangfuseHandler(labelLangfuseHandler);
2100
2122
  }
2101
2123
  }
2124
+
2125
+ /**
2126
+ * Generates one parent summary for two or more logical activities. The
2127
+ * summary model is traced as a dedicated activity-phase agent root in the
2128
+ * conversation session, with the model callback recorded as its generation
2129
+ * child. No session id means no phase trace, avoiding orphan observations.
2130
+ */
2131
+ async generateActivityPhaseLabel({
2132
+ provider,
2133
+ clientOptions,
2134
+ activities,
2135
+ totalActivityCount,
2136
+ assistantContext,
2137
+ closingTextPhase,
2138
+ prompt,
2139
+ charLimit = 600,
2140
+ chainOptions,
2141
+ traceSeed,
2142
+ sourceRunId,
2143
+ sourceTraceId,
2144
+ responseId,
2145
+ phaseIndex,
2146
+ status = 'completed',
2147
+ agentIds,
2148
+ }: t.RunActivityPhaseLabelOptions): Promise<{ label?: string }> {
2149
+ if (activities.length < 2) {
2150
+ return {};
2151
+ }
2152
+
2153
+ const phaseSeq = ++this.activityPhaseLabelSeq;
2154
+ const hasUnattributedActivity = activities.some(
2155
+ (activity) => activity.agentId == null
2156
+ );
2157
+ const hasOmittedActivitiesWithoutAgentIds =
2158
+ agentIds == null &&
2159
+ (totalActivityCount ?? activities.length) > activities.length;
2160
+ const contributingAgentIds = [
2161
+ ...new Set([
2162
+ ...(agentIds ?? []),
2163
+ ...activities.flatMap((activity) =>
2164
+ activity.agentId == null ? [] : [activity.agentId]
2165
+ ),
2166
+ ]),
2167
+ ];
2168
+ const agentContexts = this.Graph?.agentContexts;
2169
+ if (
2170
+ contributingAgentIds.some(
2171
+ (agentId) => agentContexts?.get(agentId) == null
2172
+ )
2173
+ ) {
2174
+ return {};
2175
+ }
2176
+ const phaseContext =
2177
+ this.Graph == null
2178
+ ? undefined
2179
+ : this.Graph.agentContexts.get(this.Graph.defaultAgentId);
2180
+ const phaseLangfuseConfig = resolveLangfuseConfig(
2181
+ this.langfuse,
2182
+ phaseContext?.langfuse
2183
+ );
2184
+
2185
+ let redaction = hasToolOutputTracingConfig(
2186
+ this.langfuse,
2187
+ phaseContext?.langfuse
2188
+ )
2189
+ ? resolveToolOutputTracingConfig(this.langfuse, phaseContext?.langfuse)
2190
+ : undefined;
2191
+ const redactionContexts =
2192
+ contributingAgentIds.length > 0 &&
2193
+ !hasUnattributedActivity &&
2194
+ !hasOmittedActivitiesWithoutAgentIds
2195
+ ? contributingAgentIds.flatMap((agentId) => {
2196
+ const context = agentContexts?.get(agentId);
2197
+ return context == null ? [] : [context];
2198
+ })
2199
+ : Array.from(agentContexts?.values() ?? []);
2200
+ for (const context of redactionContexts) {
2201
+ if (!hasToolOutputTracingConfig(this.langfuse, context.langfuse)) {
2202
+ continue;
2203
+ }
2204
+ const candidate = resolveToolOutputTracingConfig(
2205
+ this.langfuse,
2206
+ context.langfuse
2207
+ );
2208
+ if (redaction == null) {
2209
+ redaction = candidate;
2210
+ continue;
2211
+ }
2212
+ redaction = {
2213
+ enabled: redaction.enabled === false ? false : candidate.enabled,
2214
+ redactedToolNames: new Set([
2215
+ ...redaction.redactedToolNames,
2216
+ ...candidate.redactedToolNames,
2217
+ ]),
2218
+ redactedToolNameMatchMode:
2219
+ redaction.redactedToolNameMatchMode === 'partial' ||
2220
+ candidate.redactedToolNameMatchMode === 'partial'
2221
+ ? 'partial'
2222
+ : 'exact',
2223
+ redactionText: redaction.redactionText,
2224
+ };
2225
+ }
2226
+
2227
+ const userPrompt = buildActivityPhaseLabelPrompt({
2228
+ activities,
2229
+ totalActivityCount,
2230
+ charLimit,
2231
+ assistantContext,
2232
+ redaction,
2233
+ });
2234
+ if (userPrompt === '') {
2235
+ return {};
2236
+ }
2237
+ const phaseChainOptions = {
2238
+ ...(chainOptions ?? {}),
2239
+ } as Partial<RunnableConfig> & {
2240
+ configurable?: Record<string, unknown> & {
2241
+ requestBody?: { parentMessageId?: unknown };
2242
+ };
2243
+ };
2244
+ const phaseUserId =
2245
+ typeof phaseChainOptions.configurable?.user_id === 'string'
2246
+ ? phaseChainOptions.configurable.user_id
2247
+ : undefined;
2248
+ const phaseSessionId =
2249
+ typeof phaseChainOptions.configurable?.thread_id === 'string'
2250
+ ? phaseChainOptions.configurable.thread_id
2251
+ : undefined;
2252
+ const resolvedPhaseIndex = phaseIndex ?? phaseSeq - 1;
2253
+ const phaseMessageId =
2254
+ responseId ?? `activity-phase-${this.id}-${resolvedPhaseIndex}`;
2255
+ const phaseAgentId = this.Graph?.defaultAgentId;
2256
+ const phaseAgentName = phaseContext?.name;
2257
+ const phaseParentMessageId =
2258
+ phaseChainOptions.configurable?.requestBody?.parentMessageId;
2259
+ const phaseMetadata: Record<string, unknown> = {
2260
+ sourceRunId: sourceRunId ?? this.id,
2261
+ ...(sourceTraceId == null ? {} : { sourceTraceId }),
2262
+ responseId: phaseMessageId,
2263
+ phaseIndex: resolvedPhaseIndex,
2264
+ activityCount: Math.max(activities.length, totalActivityCount ?? 0),
2265
+ status,
2266
+ contributingAgentIds,
2267
+ ...(typeof phaseParentMessageId === 'string'
2268
+ ? { parentMessageId: phaseParentMessageId }
2269
+ : {}),
2270
+ ...(phaseAgentId == null ? {} : { agentId: phaseAgentId }),
2271
+ ...(phaseAgentName == null ? {} : { agentName: phaseAgentName }),
2272
+ ...(closingTextPhase == null ? {} : { closingTextPhase }),
2273
+ };
2274
+ const traceMetadata = {
2275
+ ...createLangfuseTraceMetadata({
2276
+ messageId: phaseMessageId,
2277
+ parentMessageId: phaseParentMessageId,
2278
+ agentId: phaseAgentId,
2279
+ agentName: phaseAgentName,
2280
+ }),
2281
+ sourceRunId: String(phaseMetadata.sourceRunId),
2282
+ ...(sourceTraceId == null ? {} : { sourceTraceId }),
2283
+ responseId: phaseMessageId,
2284
+ phaseIndex: String(resolvedPhaseIndex),
2285
+ activityCount: String(phaseMetadata.activityCount),
2286
+ status,
2287
+ ...(contributingAgentIds.length === 0
2288
+ ? {}
2289
+ : { contributingAgentIds: contributingAgentIds.join(',') }),
2290
+ ...(closingTextPhase == null ? {} : { closingTextPhase }),
2291
+ };
2292
+ const phaseRunName = getLangfuseTraceName(
2293
+ traceMetadata,
2294
+ 'LibreChat Activity Phase'
2295
+ );
2296
+ const phaseTraceName = phaseChainOptions.runName ?? phaseRunName;
2297
+ const phaseTags = [
2298
+ 'librechat',
2299
+ 'activity-phase',
2300
+ 'agent-run-summary',
2301
+ 'agent',
2302
+ ];
2303
+ initializeLangfuseTracing(phaseLangfuseConfig);
2304
+
2305
+ const inheritedTraceSeed = getTraceIdSeed();
2306
+ const phaseTraceSeed =
2307
+ phaseLangfuseConfig?.deterministicTraceId === true ||
2308
+ inheritedTraceSeed != null
2309
+ ? (traceSeed ?? `activity-phase-${this.id}-${resolvedPhaseIndex}`)
2310
+ : undefined;
2311
+ const phaseScopeRunId = `activity-phase:${this.id}:${phaseSeq}:${nanoid()}`;
2312
+ const phaseRuntimeScope = resolveLangfuseRuntimeScope({
2313
+ runLangfuse: this.langfuse,
2314
+ langfuseOverlay: phaseContext?.langfuse,
2315
+ traceIdSeed: phaseTraceSeed,
2316
+ runId: phaseScopeRunId,
2317
+ });
2318
+ let phaseLangfuseHandler: CallbackEntry | undefined;
2319
+ const sourceUserText =
2320
+ this.activityPhaseTraceInput ??
2321
+ findActivityPhaseTraceInput(this.Graph?.getRunMessages() ?? []);
2322
+ if (phaseSessionId != null && sourceUserText != null) {
2323
+ phaseLangfuseHandler = createLangfuseHandler({
2324
+ langfuse: phaseLangfuseConfig,
2325
+ userId: phaseUserId,
2326
+ sessionId: phaseSessionId,
2327
+ traceMetadata,
2328
+ tags: phaseTags,
2329
+ traceIdSeed:
2330
+ phaseLangfuseConfig?.deterministicTraceId === true
2331
+ ? phaseTraceSeed
2332
+ : undefined,
2333
+ runId: phaseScopeRunId,
2334
+ toolOutputTracing: phaseRuntimeScope.toolOutputTracing,
2335
+ traceName: phaseTraceName,
2336
+ });
2337
+ }
2338
+ if (phaseLangfuseHandler != null) {
2339
+ phaseChainOptions.callbacks = appendCallbacks(
2340
+ phaseChainOptions.callbacks,
2341
+ [phaseLangfuseHandler]
2342
+ );
2343
+ }
2344
+
2345
+ const model = initializeModel({
2346
+ provider,
2347
+ clientOptions: {
2348
+ ...(clientOptions ?? {}),
2349
+ streaming: false,
2350
+ } as t.ClientOptions,
2351
+ }) as t.ChatModelInstance;
2352
+ const phaseRunId = `${this.id}-activity-phase-${phaseSeq}`;
2353
+ const invokeConfig = Object.assign({}, phaseChainOptions, {
2354
+ run_id: phaseRunId,
2355
+ runId: phaseRunId,
2356
+ runName: 'summarize-activity-phase',
2357
+ tags: [...new Set([...(phaseChainOptions.tags ?? []), ...phaseTags])],
2358
+ metadata: {
2359
+ ...(phaseChainOptions.metadata ?? {}),
2360
+ ...phaseMetadata,
2361
+ },
2362
+ }) as Partial<RunnableConfig>;
2363
+ const invokeModel = (
2364
+ runtimeConfig: Partial<RunnableConfig>
2365
+ ): Promise<unknown> =>
2366
+ model.invoke(
2367
+ [
2368
+ new SystemMessage(prompt ?? ACTIVITY_PHASE_LABEL_PROMPT),
2369
+ new HumanMessage(userPrompt),
2370
+ ],
2371
+ runtimeConfig
2372
+ );
2373
+ const invokeWithCallbackFallback = async (
2374
+ runtimeConfig: Partial<RunnableConfig>
2375
+ ): Promise<unknown> => {
2376
+ try {
2377
+ return await invokeModel(runtimeConfig);
2378
+ } catch (error) {
2379
+ const aborted =
2380
+ (runtimeConfig as { signal?: AbortSignal }).signal?.aborted ===
2381
+ true || (error as Error | null)?.name === 'AbortError';
2382
+ const callbackFailure = /callback|tracer|event.?stream/i.test(
2383
+ String(
2384
+ (error as Error | null)?.stack ??
2385
+ (error as Error | null)?.message ??
2386
+ ''
2387
+ )
2388
+ );
2389
+ if (aborted || !callbackFailure) {
2390
+ throw error;
2391
+ }
2392
+ const langfuseHandler = findCallback(
2393
+ runtimeConfig.callbacks,
2394
+ isLangfuseCallbackHandler
2395
+ );
2396
+ const { callbacks, ...rest } = runtimeConfig;
2397
+ const safeConfig = Object.assign({}, rest, {
2398
+ callbacks: filterCallbacks(
2399
+ callbacks,
2400
+ (callback) => callback === langfuseHandler
2401
+ ),
2402
+ });
2403
+ return invokeModel(safeConfig as Partial<RunnableConfig>);
2404
+ }
2405
+ };
2406
+ const extractPhaseLabel = (response: unknown): string => {
2407
+ const content = (response as { content?: unknown } | null)?.content;
2408
+ if (typeof content === 'string') {
2409
+ return normalizeActivityPhaseLabel(content);
2410
+ }
2411
+ if (!Array.isArray(content)) {
2412
+ return '';
2413
+ }
2414
+ return normalizeActivityPhaseLabel(
2415
+ content
2416
+ .map((block) =>
2417
+ typeof block === 'string'
2418
+ ? block
2419
+ : ((block as { text?: string }).text ?? '')
2420
+ )
2421
+ .join('')
2422
+ );
2423
+ };
2424
+ const phaseRunnable = new RunnableLambda({
2425
+ func: async (
2426
+ _input: { messages: BaseMessage[] },
2427
+ runtimeConfig?: Partial<RunnableConfig>
2428
+ ): Promise<{ label?: string; messages: BaseMessage[] }> => {
2429
+ const response = await invokeWithCallbackFallback(runtimeConfig ?? {});
2430
+ const label = extractPhaseLabel(response);
2431
+ return label.length > 0
2432
+ ? { label, messages: [new AIMessage(label)] }
2433
+ : { messages: [] };
2434
+ },
2435
+ }).withConfig({ runName: 'summarize-activity-phase' });
2436
+
2437
+ try {
2438
+ const result = await withLangfuseRuntimeScope(phaseRuntimeScope, () =>
2439
+ withLangfuseAttributes(
2440
+ {
2441
+ langfuse: phaseLangfuseConfig,
2442
+ userId: phaseUserId,
2443
+ sessionId: phaseSessionId,
2444
+ traceName: phaseTraceName,
2445
+ traceMetadata,
2446
+ tags: phaseTags,
2447
+ },
2448
+ () =>
2449
+ phaseRunnable.invoke(
2450
+ {
2451
+ messages:
2452
+ sourceUserText == null
2453
+ ? []
2454
+ : [new HumanMessage(sourceUserText)],
2455
+ },
2456
+ invokeConfig
2457
+ )
2458
+ )
2459
+ );
2460
+ return result.label == null ? {} : { label: result.label };
2461
+ } finally {
2462
+ await disposeLangfuseHandler(phaseLangfuseHandler);
2463
+ }
2464
+ }
2102
2465
  }
2103
2466
 
2104
2467
  function findLastMessageOfType(
@@ -2113,6 +2476,26 @@ function findLastMessageOfType(
2113
2476
  return undefined;
2114
2477
  }
2115
2478
 
2479
+ function findActivityPhaseTraceInput(
2480
+ messages: BaseMessage[]
2481
+ ): string | undefined {
2482
+ for (let i = messages.length - 1; i >= 0; i--) {
2483
+ const message = messages[i];
2484
+ if (
2485
+ message.getType() !== 'human' ||
2486
+ message.additional_kwargs?.role === 'system' ||
2487
+ message.additional_kwargs?.isMeta === true
2488
+ ) {
2489
+ continue;
2490
+ }
2491
+ const input = extractPromptText(message).trim();
2492
+ if (input !== '') {
2493
+ return input;
2494
+ }
2495
+ }
2496
+ return undefined;
2497
+ }
2498
+
2116
2499
  function extractPromptText(message: BaseMessage): string {
2117
2500
  const content = message.content;
2118
2501
  if (typeof content === 'string') {
@@ -2123,14 +2506,13 @@ function extractPromptText(message: BaseMessage): string {
2123
2506
  }
2124
2507
  const parts: string[] = [];
2125
2508
  for (const block of content) {
2509
+ const textBlock = block as { type?: unknown; text?: unknown } | null;
2126
2510
  if (
2127
- typeof block === 'object' &&
2128
- 'type' in block &&
2129
- block.type === 'text' &&
2130
- 'text' in block &&
2131
- typeof block.text === 'string'
2511
+ textBlock != null &&
2512
+ (textBlock.type === 'text' || textBlock.type === 'input_text') &&
2513
+ typeof textBlock.text === 'string'
2132
2514
  ) {
2133
- parts.push(block.text);
2515
+ parts.push(textBlock.text);
2134
2516
  }
2135
2517
  }
2136
2518
  return parts.join('\n');
@@ -1,6 +1,11 @@
1
1
  import type { ActivityLabelToolEntry } from '@/types/activityLabel';
2
+ import {
3
+ ACTIVITY_PHASE_PROMPT_MAX_LENGTH,
4
+ buildActivityLabelPrompt,
5
+ buildActivityPhaseLabelPrompt,
6
+ normalizeActivityPhaseLabel,
7
+ } from '@/prompts/activityLabel';
2
8
  import { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT } from '@/langfuseToolOutputTracing';
3
- import { buildActivityLabelPrompt } from '@/prompts/activityLabel';
4
9
  import { resolveToolOutputTracingConfig } from '@/langfuseConfig';
5
10
 
6
11
  const entries: ActivityLabelToolEntry[] = [
@@ -235,3 +240,120 @@ describe('buildActivityLabelPrompt redaction', () => {
235
240
  expect(prompt).toContain(LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT);
236
241
  });
237
242
  });
243
+
244
+ describe('buildActivityPhaseLabelPrompt', () => {
245
+ it('prefers committed child labels and includes bounded commentary', () => {
246
+ const prompt = buildActivityPhaseLabelPrompt({
247
+ activities: [
248
+ {
249
+ label: 'Inspected session middleware behavior',
250
+ entries: [entries[0]],
251
+ },
252
+ { label: 'Fixed refresh token validation' },
253
+ ],
254
+ assistantContext: ['I am checking the auth path before changing it.'],
255
+ charLimit: 600,
256
+ });
257
+
258
+ expect(prompt).toContain('Inspected session middleware behavior');
259
+ expect(prompt).toContain('Fixed refresh token validation');
260
+ expect(prompt).toContain('I am checking the auth path');
261
+ expect(prompt).not.toContain(entries[0].toolName);
262
+ });
263
+
264
+ it('preserves partial outcomes when a committed child label is available', () => {
265
+ const prompt = buildActivityPhaseLabelPrompt({
266
+ activities: [
267
+ {
268
+ label: 'Checked the deployment and found one unhealthy replica',
269
+ status: 'partial',
270
+ },
271
+ { label: 'Recovered the remaining replicas', status: 'success' },
272
+ ],
273
+ charLimit: 600,
274
+ });
275
+
276
+ expect(prompt).toContain(
277
+ 'partial: Checked the deployment and found one unhealthy replica'
278
+ );
279
+ expect(prompt).toContain('completed: Recovered the remaining replicas');
280
+ });
281
+
282
+ it('reports omitted activities from the host total without retaining their evidence', () => {
283
+ const prompt = buildActivityPhaseLabelPrompt({
284
+ activities: Array.from({ length: 12 }, (_, index) => ({
285
+ label: `Completed activity ${index + 1}`,
286
+ })),
287
+ totalActivityCount: 20,
288
+ charLimit: 600,
289
+ });
290
+
291
+ expect(prompt).toContain('…and 8 more activities');
292
+ });
293
+
294
+ it('rejects status-only retained activities when evidence exists only beyond the cap', () => {
295
+ const prompt = buildActivityPhaseLabelPrompt({
296
+ activities: [
297
+ ...Array.from({ length: 12 }, () => ({ status: 'success' as const })),
298
+ { label: 'This evidence is outside the retained activity window' },
299
+ ],
300
+ charLimit: 600,
301
+ });
302
+
303
+ expect(prompt).toBe('');
304
+ });
305
+
306
+ it('uses raw fallback while applying the strict redaction policy', () => {
307
+ const prompt = buildActivityPhaseLabelPrompt({
308
+ activities: [
309
+ {
310
+ thinkingExcerpts: ['Secret result quoted in reasoning'],
311
+ entries,
312
+ },
313
+ { status: 'error', entries: [entries[0]] },
314
+ ],
315
+ assistantContext: ['Secret result quoted in commentary'],
316
+ charLimit: 600,
317
+ redaction: {
318
+ enabled: true,
319
+ redactedToolNames: new Set([entries[0].toolName]),
320
+ redactedToolNameMatchMode: 'exact',
321
+ redactionText: '[REDACTED]',
322
+ },
323
+ });
324
+
325
+ expect(prompt).toContain('[REDACTED]');
326
+ expect(prompt).not.toContain('Secret result');
327
+ expect(prompt).not.toContain(String(entries[0].toolOutput));
328
+ });
329
+
330
+ it('caps aggregate phase evidence while preserving the terminal cue', () => {
331
+ const oversized = 'x'.repeat(600);
332
+ const prompt = buildActivityPhaseLabelPrompt({
333
+ activities: Array.from({ length: 12 }, (_, activityIndex) => ({
334
+ thinkingExcerpts: Array.from(
335
+ { length: 4 },
336
+ (_, excerptIndex) => `${activityIndex}-${excerptIndex}-${oversized}`
337
+ ),
338
+ entries: Array.from({ length: 6 }, (_, entryIndex) => ({
339
+ toolName: `tool_${activityIndex}_${entryIndex}`,
340
+ toolInput: oversized,
341
+ toolOutput: oversized,
342
+ status: 'success' as const,
343
+ })),
344
+ })),
345
+ assistantContext: [oversized, oversized],
346
+ charLimit: 600,
347
+ });
348
+
349
+ expect(prompt.length).toBeLessThanOrEqual(ACTIVITY_PHASE_PROMPT_MAX_LENGTH);
350
+ expect(prompt.endsWith('\n\nPhase summary:')).toBe(true);
351
+ });
352
+
353
+ it('normalizes phase summaries to one bounded row', () => {
354
+ expect(normalizeActivityPhaseLabel('"Fixed auth\nrefresh handling."')).toBe(
355
+ 'Fixed auth refresh handling'
356
+ );
357
+ expect(normalizeActivityPhaseLabel('x'.repeat(300))).toHaveLength(160);
358
+ });
359
+ });