@librechat/agents 3.4.2 → 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.
- package/dist/cjs/graphs/Graph.cjs +27 -14
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
- package/dist/cjs/instrumentation.cjs +3 -3
- package/dist/cjs/langfuse.cjs +3 -3
- package/dist/cjs/langfuseRuntimeScope.cjs +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +2 -2
- package/dist/cjs/main.cjs +5 -1
- package/dist/cjs/messages/assistantPhase.cjs +59 -0
- package/dist/cjs/messages/assistantPhase.cjs.map +1 -0
- package/dist/cjs/messages/index.cjs +1 -0
- package/dist/cjs/prompts/activityLabel.cjs +76 -0
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +200 -10
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/AgentSession.cjs +1 -1
- package/dist/cjs/stream.cjs +45 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +3 -3
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +81 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/callbacks.cjs +8 -0
- package/dist/cjs/utils/callbacks.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +27 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
- package/dist/esm/instrumentation.mjs +3 -3
- package/dist/esm/langfuse.mjs +3 -3
- package/dist/esm/langfuseRuntimeScope.mjs +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +2 -2
- package/dist/esm/main.mjs +3 -2
- package/dist/esm/messages/assistantPhase.mjs +57 -0
- package/dist/esm/messages/assistantPhase.mjs.map +1 -0
- package/dist/esm/messages/index.mjs +1 -0
- package/dist/esm/prompts/activityLabel.mjs +74 -1
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +202 -12
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/AgentSession.mjs +1 -1
- package/dist/esm/stream.mjs +45 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +3 -3
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +81 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/callbacks.mjs +8 -1
- package/dist/esm/utils/callbacks.mjs.map +1 -1
- package/dist/types/messages/assistantPhase.d.ts +22 -0
- package/dist/types/messages/index.d.ts +1 -0
- package/dist/types/prompts/activityLabel.d.ts +21 -1
- package/dist/types/run.d.ts +15 -2
- package/dist/types/types/activityLabel.d.ts +63 -0
- package/dist/types/types/assistantPhase.d.ts +6 -0
- package/dist/types/types/graph.d.ts +8 -1
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/stream.d.ts +11 -0
- package/dist/types/utils/callbacks.d.ts +1 -0
- package/package.json +3 -3
- package/src/graphs/Graph.ts +33 -9
- package/src/graphs/__tests__/Graph.reasoning.test.ts +57 -0
- package/src/messages/assistantPhase.test.ts +75 -0
- package/src/messages/assistantPhase.ts +91 -0
- package/src/messages/index.ts +1 -0
- package/src/prompts/activityLabel.ts +177 -1
- package/src/run.ts +403 -21
- package/src/specs/activity-label-prompt.test.ts +123 -1
- package/src/specs/activity-phase-label.test.ts +306 -0
- package/src/stream.ts +69 -12
- package/src/tools/__tests__/SubagentExecutor.test.ts +436 -0
- package/src/tools/subagent/SubagentExecutor.ts +160 -8
- package/src/types/activityLabel.ts +65 -0
- package/src/types/assistantPhase.ts +6 -0
- package/src/types/graph.ts +8 -0
- package/src/types/index.ts +1 -0
- package/src/types/stream.ts +9 -0
- 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
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
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
|
|
2107
|
+
const { callbacks, ...rest } = invokeConfig;
|
|
2089
2108
|
const safeConfig = Object.assign({}, rest, {
|
|
2090
|
-
callbacks:
|
|
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
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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(
|
|
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
|
+
});
|