@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.
Files changed (46) hide show
  1. package/dist/cjs/graphs/Graph.cjs +14 -2
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/langchain/index.cjs +12 -0
  4. package/dist/cjs/langchain/messages.cjs +12 -0
  5. package/dist/cjs/langfuseConfig.cjs +16 -0
  6. package/dist/cjs/langfuseConfig.cjs.map +1 -1
  7. package/dist/cjs/langfuseSpanRegistry.cjs +16 -4
  8. package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +12 -0
  10. package/dist/cjs/tools/SubagentTool.cjs +7 -2
  11. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  12. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +5 -1
  13. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -1
  14. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +38 -6
  15. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  16. package/dist/esm/graphs/Graph.mjs +14 -2
  17. package/dist/esm/graphs/Graph.mjs.map +1 -1
  18. package/dist/esm/langchain/index.mjs +2 -2
  19. package/dist/esm/langchain/messages.mjs +2 -2
  20. package/dist/esm/langfuseConfig.mjs +16 -0
  21. package/dist/esm/langfuseConfig.mjs.map +1 -1
  22. package/dist/esm/langfuseSpanRegistry.mjs +16 -4
  23. package/dist/esm/langfuseSpanRegistry.mjs.map +1 -1
  24. package/dist/esm/main.mjs +2 -2
  25. package/dist/esm/tools/SubagentTool.mjs +7 -2
  26. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  27. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +5 -1
  28. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -1
  29. package/dist/esm/tools/subagent/SubagentExecutor.mjs +38 -6
  30. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  31. package/dist/types/langchain/messages.d.ts +2 -2
  32. package/dist/types/langfuseSpanRegistry.d.ts +9 -4
  33. package/dist/types/tools/SubagentTool.d.ts +2 -1
  34. package/dist/types/tools/subagent/SubagentExecutor.d.ts +4 -0
  35. package/dist/types/types/graph.d.ts +11 -0
  36. package/dist/types/types/subagentTasks.d.ts +35 -3
  37. package/package.json +1 -1
  38. package/src/graphs/Graph.ts +22 -1
  39. package/src/langchain/messages.ts +3 -0
  40. package/src/langfuseConfig.ts +43 -0
  41. package/src/langfuseSpanRegistry.ts +42 -4
  42. package/src/tools/SubagentTool.ts +18 -2
  43. package/src/tools/subagent/InMemorySubagentTaskStore.ts +9 -1
  44. package/src/tools/subagent/SubagentExecutor.ts +75 -5
  45. package/src/types/graph.ts +11 -0
  46. 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 `toolOutputTracing` are
10
- * deliberately excluded: two spans exporting to the same project under
11
- * different redaction settings still share a destination and may parent one
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-conversation execution within its trusted scope. */
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/conversation
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.6.4",
3
+ "version": "3.6.6",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -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(executeParams);
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';
@@ -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 `toolOutputTracing` are
118
- * deliberately excluded: two spans exporting to the same project under
119
- * different redaction settings still share a destination and may parent one
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: nanoid(),
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(stableStringify({ description, subagentType }))
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
- params,
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: {
@@ -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