@librechat/agents 3.6.4 → 3.6.6
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 +14 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langchain/index.cjs +12 -0
- package/dist/cjs/langchain/messages.cjs +12 -0
- package/dist/cjs/langfuseConfig.cjs +16 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +16 -4
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
- package/dist/cjs/main.cjs +12 -0
- package/dist/cjs/tools/SubagentTool.cjs +7 -2
- package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +5 -1
- package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +38 -6
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +14 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langchain/index.mjs +2 -2
- package/dist/esm/langchain/messages.mjs +2 -2
- package/dist/esm/langfuseConfig.mjs +16 -0
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +16 -4
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/tools/SubagentTool.mjs +7 -2
- package/dist/esm/tools/SubagentTool.mjs.map +1 -1
- package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +5 -1
- package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +38 -6
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/types/langchain/messages.d.ts +2 -2
- package/dist/types/langfuseSpanRegistry.d.ts +9 -4
- package/dist/types/tools/SubagentTool.d.ts +2 -1
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +4 -0
- package/dist/types/types/graph.d.ts +11 -0
- package/dist/types/types/subagentTasks.d.ts +35 -3
- package/package.json +1 -1
- package/src/graphs/Graph.ts +22 -1
- package/src/langchain/messages.ts +3 -0
- package/src/langfuseConfig.ts +43 -0
- package/src/langfuseSpanRegistry.ts +42 -4
- package/src/tools/SubagentTool.ts +18 -2
- package/src/tools/subagent/InMemorySubagentTaskStore.ts +9 -1
- package/src/tools/subagent/SubagentExecutor.ts +75 -5
- package/src/types/graph.ts +11 -0
- package/src/types/subagentTasks.ts +36 -3
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { AIMessage, AIMessageChunk, BaseMessage, BaseMessageChunk, HumanMessage, SystemMessage, ToolMessage, getBufferString, isAIMessage, isBaseMessage, isToolMessage, } from '@langchain/core/messages';
|
|
2
|
-
export type { BaseMessageFields, MessageContent, MessageContentText, MessageContentImageUrl, UsageMetadata, } from '@langchain/core/messages';
|
|
1
|
+
export { AIMessage, AIMessageChunk, BaseMessage, BaseMessageChunk, HumanMessage, SystemMessage, ToolMessage, getBufferString, isAIMessage, isBaseMessage, isToolMessage, mapChatMessagesToStoredMessages, mapStoredMessagesToChatMessages, } from '@langchain/core/messages';
|
|
2
|
+
export type { BaseMessageFields, MessageContent, MessageContentText, MessageContentImageUrl, StoredMessage, UsageMetadata, } from '@langchain/core/messages';
|
|
@@ -6,10 +6,15 @@ export declare function getLangfuseManagedSpanDestination(span: Span): string |
|
|
|
6
6
|
export declare function getLangfuseSpanProcessorParams(langfuse?: t.LangfuseConfig): LangfuseSpanProcessorParams | undefined;
|
|
7
7
|
/**
|
|
8
8
|
* Identity of an export destination (project credentials + endpoint +
|
|
9
|
-
* environment) only. Processor-level policies like
|
|
10
|
-
* deliberately excluded: two spans exporting to the
|
|
11
|
-
* different redaction settings still share a destination
|
|
12
|
-
* another.
|
|
9
|
+
* environment + custom headers) only. Processor-level policies like
|
|
10
|
+
* `toolOutputTracing` are deliberately excluded: two spans exporting to the
|
|
11
|
+
* same project under different redaction settings still share a destination
|
|
12
|
+
* and may parent one another.
|
|
13
|
+
*
|
|
14
|
+
* Custom headers are included because a gateway may route on them, making two
|
|
15
|
+
* otherwise-identical configs different projects. Treating them as part of the
|
|
16
|
+
* destination keeps a run from inheriting a parent span bound elsewhere, and
|
|
17
|
+
* keeps a rotated proxy credential from reusing the stale exporter.
|
|
13
18
|
*/
|
|
14
19
|
export declare function getLangfuseDestinationKey(params: LangfuseSpanProcessorParams): string;
|
|
15
20
|
/** The export destination a run with this config resolves to, or `undefined`
|
|
@@ -2,7 +2,7 @@ import type { JsonSchemaType, LCTool } from '@/types/tools';
|
|
|
2
2
|
import type { SubagentConfig } from '@/types';
|
|
3
3
|
import { Constants } from '@/common';
|
|
4
4
|
export declare const SubagentToolName = Constants.SUBAGENT;
|
|
5
|
-
export declare const SubagentToolDescription = "Delegate a task to a specialized subagent or bounded agent team that runs in an isolated context window. The delegated execution returns only its designated final text result \u2014 all intermediate tool calls, reasoning, and context stay isolated.\n\nWHEN TO USE:\n- The task is self-contained and can be described in a single prompt.\n- You want to offload verbose or exploratory work without bloating your own context.\n- A specialized subagent is available for the task domain.\n\nWHAT HAPPENS:\n- A fresh agent or configured agent graph is created with the task description as its only input.\n- The delegated agent or team runs to completion using isolated tools and context.\n- Only the single agent's final response or the graph's designated result-agent response is returned to you.\n\nCONSTRAINTS:\n- subagent_type must match one of the available types listed below.\n- The subagent cannot see your conversation history.";
|
|
5
|
+
export declare const SubagentToolDescription = "Delegate a task to a specialized subagent or bounded agent team that runs in an isolated context window. The delegated execution returns only its designated final text result \u2014 all intermediate tool calls, reasoning, and context stay isolated.\n\nWHEN TO USE:\n- The task is self-contained and can be described in a single prompt.\n- You want to offload verbose or exploratory work without bloating your own context.\n- A specialized subagent is available for the task domain.\n\nWHAT HAPPENS:\n- A fresh agent or configured agent graph is created with the task description as its only input, unless the host enables child-thread continuation and you provide a saved thread id.\n- The delegated agent or team runs to completion using isolated tools and context.\n- Only the single agent's final response or the graph's designated result-agent response is returned to you.\n\nCONSTRAINTS:\n- subagent_type must match one of the available types listed below.\n- The subagent cannot see your conversation history.";
|
|
6
6
|
export declare const SubagentToolSchema: {
|
|
7
7
|
readonly type: "object";
|
|
8
8
|
readonly properties: {
|
|
@@ -34,6 +34,7 @@ export declare const SubagentToolDefinition: LCTool;
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function buildSubagentToolParams(configs: SubagentConfig[], options?: {
|
|
36
36
|
background?: boolean;
|
|
37
|
+
threadContinuation?: boolean;
|
|
37
38
|
}): {
|
|
38
39
|
name: string;
|
|
39
40
|
schema: JsonSchemaType;
|
|
@@ -12,6 +12,8 @@ export declare const DEFAULT_SUBAGENT_DESCRIPTION = "No task description provide
|
|
|
12
12
|
export type SubagentExecuteParams = {
|
|
13
13
|
description: string;
|
|
14
14
|
subagentType: string;
|
|
15
|
+
/** Saved logical child thread selected by the parent model. */
|
|
16
|
+
subagentThreadId?: string;
|
|
15
17
|
threadId?: string;
|
|
16
18
|
/** Signal attached to this specific parent tool invocation. */
|
|
17
19
|
signal?: AbortSignal;
|
|
@@ -66,6 +68,8 @@ export type SubagentExecuteParams = {
|
|
|
66
68
|
hookSessionId?: string;
|
|
67
69
|
/** Process-local task controls consumed by the child graph. @internal */
|
|
68
70
|
taskRuntime?: SubagentTaskRuntime;
|
|
71
|
+
/** Host-restored child transcript for a fresh continuation run. @internal */
|
|
72
|
+
initialMessages?: BaseMessage[];
|
|
69
73
|
};
|
|
70
74
|
export type SubagentExecuteResult = {
|
|
71
75
|
content: string;
|
|
@@ -615,6 +615,17 @@ export interface LangfuseConfig {
|
|
|
615
615
|
* payloads. Defaults to the Langfuse SDK behavior.
|
|
616
616
|
*/
|
|
617
617
|
mediaUploadEnabled?: boolean;
|
|
618
|
+
/**
|
|
619
|
+
* Extra HTTP headers sent on every Langfuse export request (OTLP trace
|
|
620
|
+
* export and media uploads), for self-hosted instances behind an
|
|
621
|
+
* authenticating proxy or gateway.
|
|
622
|
+
*
|
|
623
|
+
* Deployment-level only: one exporter is shared by every span routed to a
|
|
624
|
+
* destination, so these cannot carry per-request or per-user identity.
|
|
625
|
+
* Headers participate in destination identity — see
|
|
626
|
+
* `getLangfuseDestinationKey`.
|
|
627
|
+
*/
|
|
628
|
+
additionalHeaders?: Record<string, string>;
|
|
618
629
|
metadata?: Record<string, string | number | boolean | null | undefined>;
|
|
619
630
|
/**
|
|
620
631
|
* Internal OTLP span attributes to attach to Langfuse observations before
|
|
@@ -24,8 +24,14 @@ export interface SubagentTaskProgress {
|
|
|
24
24
|
}
|
|
25
25
|
/** Read-only task metadata. Results are exposed only through `claim`. */
|
|
26
26
|
export interface SubagentTaskSnapshot {
|
|
27
|
-
/** Handle for this child-
|
|
27
|
+
/** Handle for this child-thread execution lease within its trusted scope. */
|
|
28
28
|
taskId: string;
|
|
29
|
+
/**
|
|
30
|
+
* Stable logical thread identity shared by fresh execution leases. Required
|
|
31
|
+
* from stores that advertise `supportsThreadContinuation`; optional for
|
|
32
|
+
* legacy/process-local stores that do not expose a durable conversation.
|
|
33
|
+
*/
|
|
34
|
+
threadId?: string;
|
|
29
35
|
subagentType: string;
|
|
30
36
|
status: SubagentTaskStatus;
|
|
31
37
|
createdAt: number;
|
|
@@ -95,15 +101,31 @@ export interface SubagentTaskRuntime {
|
|
|
95
101
|
export interface SubagentTaskStartRequest {
|
|
96
102
|
scopeId: string;
|
|
97
103
|
idempotencyKey: string;
|
|
104
|
+
/** Host/SDK-owned parent run identity for durable lineage. */
|
|
105
|
+
parentRunId: string;
|
|
106
|
+
/** Executing parent agent, when the graph has a stable agent identity. */
|
|
107
|
+
parentAgentId?: string;
|
|
108
|
+
/** Provider tool-call identity that created this execution lease. */
|
|
109
|
+
parentToolCallId: string;
|
|
110
|
+
/**
|
|
111
|
+
* Untrusted child-thread selector supplied by the parent model. A
|
|
112
|
+
* continuation-capable store MUST validate ownership, scope, lineage, and
|
|
113
|
+
* `subagentType` before loading any saved messages.
|
|
114
|
+
*/
|
|
115
|
+
threadId?: string;
|
|
116
|
+
/** Untrusted new user-turn text for host persistence and audit. */
|
|
117
|
+
input: string;
|
|
98
118
|
/** Stable hash of model-writable inputs used to reject conflicting replays. */
|
|
99
119
|
requestFingerprint?: string;
|
|
120
|
+
/** Execution shape selected from the host-provided subagent catalog. */
|
|
121
|
+
subagentKind: SubagentUpdateEvent['subagentKind'];
|
|
100
122
|
subagentType: string;
|
|
101
123
|
/**
|
|
102
124
|
* Starts one ephemeral execution lease. The canonical child transcript is
|
|
103
125
|
* returned so a host-owned store may persist it for a later fresh run;
|
|
104
126
|
* retaining a graph/checkpoint after terminal completion is unnecessary.
|
|
105
127
|
*/
|
|
106
|
-
run(runtime: SubagentTaskRuntime): Promise<{
|
|
128
|
+
run(runtime: SubagentTaskRuntime, initialMessages?: BaseMessage[]): Promise<{
|
|
107
129
|
content: string;
|
|
108
130
|
messages?: BaseMessage[];
|
|
109
131
|
}>;
|
|
@@ -115,6 +137,9 @@ export type SubagentTaskStartResult = {
|
|
|
115
137
|
} | {
|
|
116
138
|
accepted: false;
|
|
117
139
|
reason: 'capacity';
|
|
140
|
+
} | {
|
|
141
|
+
accepted: false;
|
|
142
|
+
reason: 'thread_unavailable';
|
|
118
143
|
} | {
|
|
119
144
|
accepted: false;
|
|
120
145
|
reason: 'conflict';
|
|
@@ -123,10 +148,17 @@ export type SubagentTaskStartResult = {
|
|
|
123
148
|
/**
|
|
124
149
|
* Host-replaceable store contract used by the SDK's subagent tool. The store
|
|
125
150
|
* should normally outlive individual `Run` instances. Durable hosts may
|
|
126
|
-
* persist the transcript returned by `run` under the task/
|
|
151
|
+
* persist the transcript returned by `run` under the task/thread
|
|
127
152
|
* lineage and start a fresh execution for a later turn.
|
|
128
153
|
*/
|
|
129
154
|
export interface SubagentTaskStore {
|
|
155
|
+
/**
|
|
156
|
+
* True only when `start` authorizes an existing `threadId`, loads its
|
|
157
|
+
* saved transcript, and supplies that transcript to `run`. The flag exposes
|
|
158
|
+
* the model-facing continuation field, so a store must fail closed for an
|
|
159
|
+
* unknown or unauthorized id rather than starting an empty child.
|
|
160
|
+
*/
|
|
161
|
+
readonly supportsThreadContinuation?: boolean;
|
|
130
162
|
start(request: SubagentTaskStartRequest): SubagentTaskStartResult;
|
|
131
163
|
get(scopeId: string, taskId: string): SubagentTaskSnapshot | undefined;
|
|
132
164
|
list(scopeId: string): SubagentTaskSnapshot[];
|
package/package.json
CHANGED
package/src/graphs/Graph.ts
CHANGED
|
@@ -4854,6 +4854,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
4854
4854
|
const input = rawInput as {
|
|
4855
4855
|
description?: string;
|
|
4856
4856
|
subagent_type?: string;
|
|
4857
|
+
subagent_thread_id?: string;
|
|
4857
4858
|
run_in_background?: boolean;
|
|
4858
4859
|
};
|
|
4859
4860
|
const description =
|
|
@@ -4863,6 +4864,11 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
4863
4864
|
: DEFAULT_SUBAGENT_DESCRIPTION;
|
|
4864
4865
|
const subagentType =
|
|
4865
4866
|
typeof input.subagent_type === 'string' ? input.subagent_type : '';
|
|
4867
|
+
const subagentThreadId =
|
|
4868
|
+
typeof input.subagent_thread_id === 'string' &&
|
|
4869
|
+
input.subagent_thread_id.trim() !== ''
|
|
4870
|
+
? input.subagent_thread_id.trim()
|
|
4871
|
+
: undefined;
|
|
4866
4872
|
const threadId = config.configurable?.thread_id as string | undefined;
|
|
4867
4873
|
/** Surface the parent call id so child checkpoints, interrupts, and
|
|
4868
4874
|
* update events remain correlated across replay and resume. */
|
|
@@ -4905,12 +4911,27 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
4905
4911
|
| undefined,
|
|
4906
4912
|
};
|
|
4907
4913
|
if (input.run_in_background === true) {
|
|
4908
|
-
return executor.executeInBackground(
|
|
4914
|
+
return executor.executeInBackground({
|
|
4915
|
+
...executeParams,
|
|
4916
|
+
...(subagentThreadId == null
|
|
4917
|
+
? {}
|
|
4918
|
+
: { subagentThreadId }),
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
if (subagentThreadId != null) {
|
|
4922
|
+
return JSON.stringify({
|
|
4923
|
+
status: 'rejected',
|
|
4924
|
+
tool: Constants.SUBAGENT,
|
|
4925
|
+
message:
|
|
4926
|
+
'Child-thread continuation requires run_in_background.',
|
|
4927
|
+
});
|
|
4909
4928
|
}
|
|
4910
4929
|
const result = await executor.execute(executeParams);
|
|
4911
4930
|
return result.content;
|
|
4912
4931
|
}, buildSubagentToolParams(executableConfigs, {
|
|
4913
4932
|
background: this.subagentTasks != null,
|
|
4933
|
+
threadContinuation:
|
|
4934
|
+
this.subagentTasks?.store.supportsThreadContinuation === true,
|
|
4914
4935
|
}));
|
|
4915
4936
|
const replayableSubagentTool = subagentTool as typeof subagentTool &
|
|
4916
4937
|
ReplayableSubagentTool;
|
|
@@ -10,6 +10,8 @@ export {
|
|
|
10
10
|
isAIMessage,
|
|
11
11
|
isBaseMessage,
|
|
12
12
|
isToolMessage,
|
|
13
|
+
mapChatMessagesToStoredMessages,
|
|
14
|
+
mapStoredMessagesToChatMessages,
|
|
13
15
|
} from '@langchain/core/messages';
|
|
14
16
|
|
|
15
17
|
export type {
|
|
@@ -17,5 +19,6 @@ export type {
|
|
|
17
19
|
MessageContent,
|
|
18
20
|
MessageContentText,
|
|
19
21
|
MessageContentImageUrl,
|
|
22
|
+
StoredMessage,
|
|
20
23
|
UsageMetadata,
|
|
21
24
|
} from '@langchain/core/messages';
|
package/src/langfuseConfig.ts
CHANGED
|
@@ -175,6 +175,44 @@ export function resolveToolOutputTracingConfig(
|
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Merges header maps case-insensitively, keeping the override's casing.
|
|
180
|
+
*
|
|
181
|
+
* A plain spread would keep both `X-Proxy-Token` and `x-proxy-token`, and
|
|
182
|
+
* filling a fetch `Headers` from that record *appends* rather than replaces —
|
|
183
|
+
* the exporter would send one comma-joined `run-token, agent-token` value, so
|
|
184
|
+
* the agent override never cleanly wins and a gateway sees a malformed
|
|
185
|
+
* credential. Matches the case-insensitive identity already used for the
|
|
186
|
+
* destination key.
|
|
187
|
+
*/
|
|
188
|
+
function mergeAdditionalHeaders(
|
|
189
|
+
base?: Record<string, string>,
|
|
190
|
+
override?: Record<string, string>
|
|
191
|
+
): Record<string, string> | undefined {
|
|
192
|
+
if (base == null && override == null) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const merged: Record<string, string> = { ...base };
|
|
197
|
+
if (override == null) {
|
|
198
|
+
return merged;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const baseKeyByLower = new Map<string, string>(
|
|
202
|
+
Object.keys(merged).map((key) => [key.toLowerCase(), key])
|
|
203
|
+
);
|
|
204
|
+
for (const [key, value] of Object.entries(override)) {
|
|
205
|
+
const lower = key.toLowerCase();
|
|
206
|
+
const existingKey = baseKeyByLower.get(lower);
|
|
207
|
+
if (existingKey != null && existingKey !== key) {
|
|
208
|
+
delete merged[existingKey];
|
|
209
|
+
}
|
|
210
|
+
merged[key] = value;
|
|
211
|
+
baseKeyByLower.set(lower, key);
|
|
212
|
+
}
|
|
213
|
+
return merged;
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
export function resolveLangfuseConfig(
|
|
179
217
|
runLangfuse?: t.LangfuseConfig,
|
|
180
218
|
agentLangfuse?: t.LangfuseConfig
|
|
@@ -208,6 +246,10 @@ export function resolveLangfuseConfig(
|
|
|
208
246
|
...agentLangfuse.metadata,
|
|
209
247
|
}
|
|
210
248
|
: undefined;
|
|
249
|
+
const additionalHeaders = mergeAdditionalHeaders(
|
|
250
|
+
runLangfuse.additionalHeaders,
|
|
251
|
+
agentLangfuse.additionalHeaders
|
|
252
|
+
);
|
|
211
253
|
const librechatTraceAttributes =
|
|
212
254
|
runLangfuse.librechatTraceAttributes != null ||
|
|
213
255
|
agentLangfuse.librechatTraceAttributes != null
|
|
@@ -230,6 +272,7 @@ export function resolveLangfuseConfig(
|
|
|
230
272
|
...runLangfuse,
|
|
231
273
|
...agentLangfuse,
|
|
232
274
|
...(metadata != null ? { metadata } : {}),
|
|
275
|
+
...(additionalHeaders != null ? { additionalHeaders } : {}),
|
|
233
276
|
...(librechatTraceAttributes != null ? { librechatTraceAttributes } : {}),
|
|
234
277
|
...(tags != null ? { tags } : {}),
|
|
235
278
|
...(toolNodeTracing != null ? { toolNodeTracing } : {}),
|
|
@@ -59,6 +59,12 @@ function resolveLangfuseEnvironment(
|
|
|
59
59
|
return undefined;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function hasAdditionalHeaders(
|
|
63
|
+
headers?: Record<string, string>
|
|
64
|
+
): headers is Record<string, string> {
|
|
65
|
+
return headers != null && Object.keys(headers).length > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
export function getLangfuseSpanProcessorParams(
|
|
63
69
|
langfuse?: t.LangfuseConfig
|
|
64
70
|
): LangfuseSpanProcessorParams | undefined {
|
|
@@ -66,6 +72,9 @@ export function getLangfuseSpanProcessorParams(
|
|
|
66
72
|
return undefined;
|
|
67
73
|
}
|
|
68
74
|
const environment = resolveLangfuseEnvironment(langfuse);
|
|
75
|
+
const additionalHeaders = hasAdditionalHeaders(langfuse?.additionalHeaders)
|
|
76
|
+
? { additionalHeaders: langfuse.additionalHeaders }
|
|
77
|
+
: {};
|
|
69
78
|
if (hasLangfuseConfigCredentials(langfuse)) {
|
|
70
79
|
return {
|
|
71
80
|
publicKey: langfuse.publicKey,
|
|
@@ -75,6 +84,7 @@ export function getLangfuseSpanProcessorParams(
|
|
|
75
84
|
...(langfuse.mediaUploadEnabled != null
|
|
76
85
|
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
|
|
77
86
|
: {}),
|
|
87
|
+
...additionalHeaders,
|
|
78
88
|
};
|
|
79
89
|
}
|
|
80
90
|
if (hasLangfuseEnvConfig()) {
|
|
@@ -90,6 +100,7 @@ export function getLangfuseSpanProcessorParams(
|
|
|
90
100
|
...(langfuse?.mediaUploadEnabled != null
|
|
91
101
|
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
|
|
92
102
|
: {}),
|
|
103
|
+
...additionalHeaders,
|
|
93
104
|
};
|
|
94
105
|
}
|
|
95
106
|
if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
|
|
@@ -101,6 +112,7 @@ export function getLangfuseSpanProcessorParams(
|
|
|
101
112
|
...(langfuse.mediaUploadEnabled != null
|
|
102
113
|
? { mediaUploadEnabled: langfuse.mediaUploadEnabled }
|
|
103
114
|
: {}),
|
|
115
|
+
...additionalHeaders,
|
|
104
116
|
};
|
|
105
117
|
}
|
|
106
118
|
return undefined;
|
|
@@ -112,12 +124,37 @@ function hashCacheKeyValue(value: string | undefined): string | undefined {
|
|
|
112
124
|
: undefined;
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Order- and case-insensitive digest of the custom headers sent to a
|
|
129
|
+
* destination, so header maps that differ only in key order or header-name
|
|
130
|
+
* casing resolve to one destination instead of duplicating its exporter.
|
|
131
|
+
* Hashed because these values are credentials (proxy tokens, gateway keys).
|
|
132
|
+
* Absent and empty both yield `undefined`, keeping keys stable for the
|
|
133
|
+
* overwhelmingly common no-headers case.
|
|
134
|
+
*/
|
|
135
|
+
function hashAdditionalHeaders(
|
|
136
|
+
headers: Record<string, string> | undefined
|
|
137
|
+
): string | undefined {
|
|
138
|
+
if (!hasAdditionalHeaders(headers)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const normalized = Object.entries(headers)
|
|
142
|
+
.map(([name, value]) => JSON.stringify([name.trim().toLowerCase(), value]))
|
|
143
|
+
.sort();
|
|
144
|
+
return hashCacheKeyValue(normalized.join('\n'));
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
/**
|
|
116
148
|
* Identity of an export destination (project credentials + endpoint +
|
|
117
|
-
* environment) only. Processor-level policies like
|
|
118
|
-
* deliberately excluded: two spans exporting to the
|
|
119
|
-
* different redaction settings still share a destination
|
|
120
|
-
* another.
|
|
149
|
+
* environment + custom headers) only. Processor-level policies like
|
|
150
|
+
* `toolOutputTracing` are deliberately excluded: two spans exporting to the
|
|
151
|
+
* same project under different redaction settings still share a destination
|
|
152
|
+
* and may parent one another.
|
|
153
|
+
*
|
|
154
|
+
* Custom headers are included because a gateway may route on them, making two
|
|
155
|
+
* otherwise-identical configs different projects. Treating them as part of the
|
|
156
|
+
* destination keeps a run from inheriting a parent span bound elsewhere, and
|
|
157
|
+
* keeps a rotated proxy credential from reusing the stale exporter.
|
|
121
158
|
*/
|
|
122
159
|
export function getLangfuseDestinationKey(
|
|
123
160
|
params: LangfuseSpanProcessorParams
|
|
@@ -127,6 +164,7 @@ export function getLangfuseDestinationKey(
|
|
|
127
164
|
secretKeyHash: hashCacheKeyValue(params.secretKey),
|
|
128
165
|
baseUrl: params.baseUrl,
|
|
129
166
|
environment: params.environment,
|
|
167
|
+
additionalHeadersHash: hashAdditionalHeaders(params.additionalHeaders),
|
|
130
168
|
});
|
|
131
169
|
}
|
|
132
170
|
|
|
@@ -13,7 +13,7 @@ WHEN TO USE:
|
|
|
13
13
|
- A specialized subagent is available for the task domain.
|
|
14
14
|
|
|
15
15
|
WHAT HAPPENS:
|
|
16
|
-
- A fresh agent or configured agent graph is created with the task description as its only input.
|
|
16
|
+
- A fresh agent or configured agent graph is created with the task description as its only input, unless the host enables child-thread continuation and you provide a saved thread id.
|
|
17
17
|
- The delegated agent or team runs to completion using isolated tools and context.
|
|
18
18
|
- Only the single agent's final response or the graph's designated result-agent response is returned to you.
|
|
19
19
|
|
|
@@ -30,6 +30,9 @@ const SUBAGENT_TYPE_PROP_DESCRIPTION =
|
|
|
30
30
|
const RUN_IN_BACKGROUND_PROP_DESCRIPTION =
|
|
31
31
|
'Set true to start the subagent as a detached process-local task and return a background_task_id immediately. Poll the host background-task tool to collect its result. The task can outlive this turn but does not survive a process restart.';
|
|
32
32
|
|
|
33
|
+
const SUBAGENT_THREAD_PROP_DESCRIPTION =
|
|
34
|
+
'Continue a host-owned child thread using a fresh execution lease. The saved thread must belong to this scope and subagent type. Only available with run_in_background.';
|
|
35
|
+
|
|
33
36
|
export const SubagentToolSchema = {
|
|
34
37
|
type: 'object',
|
|
35
38
|
properties: {
|
|
@@ -59,7 +62,7 @@ export const SubagentToolDefinition: LCTool = {
|
|
|
59
62
|
*/
|
|
60
63
|
export function buildSubagentToolParams(
|
|
61
64
|
configs: SubagentConfig[],
|
|
62
|
-
options: { background?: boolean } = {}
|
|
65
|
+
options: { background?: boolean; threadContinuation?: boolean } = {}
|
|
63
66
|
): {
|
|
64
67
|
name: string;
|
|
65
68
|
schema: JsonSchemaType;
|
|
@@ -93,6 +96,15 @@ export function buildSubagentToolParams(
|
|
|
93
96
|
},
|
|
94
97
|
}
|
|
95
98
|
: {}),
|
|
99
|
+
...(options.background === true &&
|
|
100
|
+
options.threadContinuation === true
|
|
101
|
+
? {
|
|
102
|
+
subagent_thread_id: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: SUBAGENT_THREAD_PROP_DESCRIPTION,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
: {}),
|
|
96
108
|
},
|
|
97
109
|
required: ['description', 'subagent_type'],
|
|
98
110
|
},
|
|
@@ -100,6 +112,10 @@ export function buildSubagentToolParams(
|
|
|
100
112
|
options.background === true
|
|
101
113
|
? '\n\nBACKGROUND EXECUTION:\n- Set run_in_background to true when you do not need the result immediately. The call returns a background_task_id; use the host background-task tools to poll, steer, queue, interrupt, or cancel it.'
|
|
102
114
|
: ''
|
|
115
|
+
}${
|
|
116
|
+
options.background === true && options.threadContinuation === true
|
|
117
|
+
? '\n- To continue a completed child later, set run_in_background and pass its subagent_thread_id. The host starts a fresh execution from saved history; it does not restore a completed runtime.'
|
|
118
|
+
: ''
|
|
103
119
|
}\n\nAvailable types:\n${typeDescriptions}`,
|
|
104
120
|
};
|
|
105
121
|
}
|
|
@@ -50,6 +50,7 @@ type StoredTask = {
|
|
|
50
50
|
idempotencyKey: string;
|
|
51
51
|
requestFingerprint?: string;
|
|
52
52
|
scopeId: string;
|
|
53
|
+
threadId: string;
|
|
53
54
|
subagentType: string;
|
|
54
55
|
status: SubagentTaskStatus;
|
|
55
56
|
createdAt: number;
|
|
@@ -167,6 +168,7 @@ function toInjectedMessage(control: PendingControl): InjectedMessage {
|
|
|
167
168
|
function snapshot(task: StoredTask): SubagentTaskSnapshot {
|
|
168
169
|
return {
|
|
169
170
|
taskId: task.id,
|
|
171
|
+
threadId: task.threadId,
|
|
170
172
|
subagentType: task.subagentType,
|
|
171
173
|
status: task.status,
|
|
172
174
|
createdAt: task.createdAt,
|
|
@@ -251,13 +253,19 @@ export class InMemorySubagentTaskStore implements SubagentTaskStore {
|
|
|
251
253
|
this.dropEmptyBucket(scopeId, bucket);
|
|
252
254
|
return { accepted: false, reason: 'capacity' };
|
|
253
255
|
}
|
|
256
|
+
const taskId = nanoid();
|
|
257
|
+
const requestedThreadId = request.threadId?.trim();
|
|
254
258
|
const task: StoredTask = {
|
|
255
|
-
id:
|
|
259
|
+
id: taskId,
|
|
256
260
|
idempotencyKey,
|
|
257
261
|
...(requestFingerprint == null || requestFingerprint === ''
|
|
258
262
|
? {}
|
|
259
263
|
: { requestFingerprint }),
|
|
260
264
|
scopeId,
|
|
265
|
+
threadId:
|
|
266
|
+
requestedThreadId != null && requestedThreadId !== ''
|
|
267
|
+
? requestedThreadId
|
|
268
|
+
: taskId,
|
|
261
269
|
subagentType: request.subagentType,
|
|
262
270
|
status: 'running',
|
|
263
271
|
createdAt: now,
|
|
@@ -623,10 +623,17 @@ function getSettlementFingerprint(output: PersistedToolOutput): string {
|
|
|
623
623
|
|
|
624
624
|
function getBackgroundTaskFingerprint(
|
|
625
625
|
description: string,
|
|
626
|
-
subagentType: string
|
|
626
|
+
subagentType: string,
|
|
627
|
+
threadId?: string
|
|
627
628
|
): string {
|
|
628
629
|
return createHash('sha256')
|
|
629
|
-
.update(
|
|
630
|
+
.update(
|
|
631
|
+
stableStringify({
|
|
632
|
+
description,
|
|
633
|
+
subagentType,
|
|
634
|
+
...(threadId == null || threadId === '' ? {} : { threadId }),
|
|
635
|
+
})
|
|
636
|
+
)
|
|
630
637
|
.digest('hex');
|
|
631
638
|
}
|
|
632
639
|
|
|
@@ -758,6 +765,8 @@ function createReplayCheckpointWorkflow(
|
|
|
758
765
|
export type SubagentExecuteParams = {
|
|
759
766
|
description: string;
|
|
760
767
|
subagentType: string;
|
|
768
|
+
/** Saved logical child thread selected by the parent model. */
|
|
769
|
+
subagentThreadId?: string;
|
|
761
770
|
threadId?: string;
|
|
762
771
|
/** Signal attached to this specific parent tool invocation. */
|
|
763
772
|
signal?: AbortSignal;
|
|
@@ -812,6 +821,8 @@ export type SubagentExecuteParams = {
|
|
|
812
821
|
hookSessionId?: string;
|
|
813
822
|
/** Process-local task controls consumed by the child graph. @internal */
|
|
814
823
|
taskRuntime?: SubagentTaskRuntime;
|
|
824
|
+
/** Host-restored child transcript for a fresh continuation run. @internal */
|
|
825
|
+
initialMessages?: BaseMessage[];
|
|
815
826
|
};
|
|
816
827
|
|
|
817
828
|
export type SubagentExecuteResult = {
|
|
@@ -1071,6 +1082,19 @@ export class SubagentExecutor {
|
|
|
1071
1082
|
'Background subagent execution requires a parent tool call ID.',
|
|
1072
1083
|
});
|
|
1073
1084
|
}
|
|
1085
|
+
const subagentThreadId = params.subagentThreadId?.trim();
|
|
1086
|
+
if (
|
|
1087
|
+
subagentThreadId != null &&
|
|
1088
|
+
subagentThreadId !== '' &&
|
|
1089
|
+
this.taskConfig.store.supportsThreadContinuation !== true
|
|
1090
|
+
) {
|
|
1091
|
+
return JSON.stringify({
|
|
1092
|
+
status: 'rejected',
|
|
1093
|
+
tool: Constants.SUBAGENT,
|
|
1094
|
+
message:
|
|
1095
|
+
'Child-thread continuation is not enabled by this host.',
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1074
1098
|
const detachedHandlers = new HandlerRegistry();
|
|
1075
1099
|
const sourceHookSessionId =
|
|
1076
1100
|
asNonEmptyString(params.parentConfigurable?.run_id) ??
|
|
@@ -1092,14 +1116,31 @@ export class SubagentExecutor {
|
|
|
1092
1116
|
this.parentAgentId ?? '',
|
|
1093
1117
|
parentToolCallId,
|
|
1094
1118
|
]),
|
|
1119
|
+
parentRunId: this.parentRunId,
|
|
1120
|
+
...(this.parentAgentId == null
|
|
1121
|
+
? {}
|
|
1122
|
+
: { parentAgentId: this.parentAgentId }),
|
|
1123
|
+
parentToolCallId,
|
|
1095
1124
|
requestFingerprint: getBackgroundTaskFingerprint(
|
|
1096
1125
|
params.description,
|
|
1097
|
-
params.subagentType
|
|
1126
|
+
params.subagentType,
|
|
1127
|
+
subagentThreadId
|
|
1098
1128
|
),
|
|
1129
|
+
...(subagentThreadId == null || subagentThreadId === ''
|
|
1130
|
+
? {}
|
|
1131
|
+
: { threadId: subagentThreadId }),
|
|
1132
|
+
input: params.description,
|
|
1133
|
+
subagentKind:
|
|
1134
|
+
'kind' in executableConfig && executableConfig.kind === 'graph'
|
|
1135
|
+
? 'graph'
|
|
1136
|
+
: 'agent',
|
|
1099
1137
|
subagentType: params.subagentType,
|
|
1100
|
-
run: (runtime) =>
|
|
1138
|
+
run: (runtime, initialMessages) =>
|
|
1101
1139
|
this.executeDetached(
|
|
1102
|
-
|
|
1140
|
+
{
|
|
1141
|
+
...params,
|
|
1142
|
+
...(initialMessages == null ? {} : { initialMessages }),
|
|
1143
|
+
},
|
|
1103
1144
|
runtime,
|
|
1104
1145
|
detachedHandlers,
|
|
1105
1146
|
taskHookRegistry,
|
|
@@ -1107,6 +1148,14 @@ export class SubagentExecutor {
|
|
|
1107
1148
|
),
|
|
1108
1149
|
});
|
|
1109
1150
|
if (!started.accepted) {
|
|
1151
|
+
if (started.reason === 'thread_unavailable') {
|
|
1152
|
+
return JSON.stringify({
|
|
1153
|
+
status: 'rejected',
|
|
1154
|
+
tool: Constants.SUBAGENT,
|
|
1155
|
+
message:
|
|
1156
|
+
'The requested subagent thread is unavailable in this parent scope. Start a new subagent thread or choose one created by this parent for the same subagent type.',
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1110
1159
|
if (started.reason === 'conflict') {
|
|
1111
1160
|
return JSON.stringify({
|
|
1112
1161
|
status: 'rejected',
|
|
@@ -1122,8 +1171,28 @@ export class SubagentExecutor {
|
|
|
1122
1171
|
'Too many background subagent tasks are already running in this scope or process. Poll or cancel an existing task, or run this call in the foreground.',
|
|
1123
1172
|
});
|
|
1124
1173
|
}
|
|
1174
|
+
const startedThreadId = started.task.threadId?.trim();
|
|
1175
|
+
if (
|
|
1176
|
+
this.taskConfig.store.supportsThreadContinuation === true &&
|
|
1177
|
+
(startedThreadId == null || startedThreadId === '')
|
|
1178
|
+
) {
|
|
1179
|
+
this.taskConfig.store.control(
|
|
1180
|
+
this.taskConfig.scopeId,
|
|
1181
|
+
started.task.taskId,
|
|
1182
|
+
{ action: 'cancel' }
|
|
1183
|
+
);
|
|
1184
|
+
return JSON.stringify({
|
|
1185
|
+
status: 'rejected',
|
|
1186
|
+
tool: Constants.SUBAGENT,
|
|
1187
|
+
message:
|
|
1188
|
+
'The host accepted the subagent task without assigning its required thread ID.',
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1125
1191
|
return JSON.stringify({
|
|
1126
1192
|
background_task_id: started.task.taskId,
|
|
1193
|
+
...(this.taskConfig.store.supportsThreadContinuation === true
|
|
1194
|
+
? { subagent_thread_id: startedThreadId }
|
|
1195
|
+
: {}),
|
|
1127
1196
|
tool: Constants.SUBAGENT,
|
|
1128
1197
|
subagent_type: params.subagentType,
|
|
1129
1198
|
status: started.task.status,
|
|
@@ -2615,6 +2684,7 @@ export class SubagentExecutor {
|
|
|
2615
2684
|
} else {
|
|
2616
2685
|
childInput = {
|
|
2617
2686
|
messages: [
|
|
2687
|
+
...(params.initialMessages ?? []),
|
|
2618
2688
|
new HumanMessage({
|
|
2619
2689
|
content: description,
|
|
2620
2690
|
additional_kwargs: {
|
package/src/types/graph.ts
CHANGED
|
@@ -787,6 +787,17 @@ export interface LangfuseConfig {
|
|
|
787
787
|
* payloads. Defaults to the Langfuse SDK behavior.
|
|
788
788
|
*/
|
|
789
789
|
mediaUploadEnabled?: boolean;
|
|
790
|
+
/**
|
|
791
|
+
* Extra HTTP headers sent on every Langfuse export request (OTLP trace
|
|
792
|
+
* export and media uploads), for self-hosted instances behind an
|
|
793
|
+
* authenticating proxy or gateway.
|
|
794
|
+
*
|
|
795
|
+
* Deployment-level only: one exporter is shared by every span routed to a
|
|
796
|
+
* destination, so these cannot carry per-request or per-user identity.
|
|
797
|
+
* Headers participate in destination identity — see
|
|
798
|
+
* `getLangfuseDestinationKey`.
|
|
799
|
+
*/
|
|
800
|
+
additionalHeaders?: Record<string, string>;
|
|
790
801
|
metadata?: Record<string, string | number | boolean | null | undefined>;
|
|
791
802
|
/**
|
|
792
803
|
* Internal OTLP span attributes to attach to Langfuse observations before
|