@librechat/agents 3.6.3 → 3.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/cjs/graphs/Graph.cjs +32 -19
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +7 -1
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/index.cjs +1 -1
  6. package/dist/cjs/main.cjs +3 -1
  7. package/dist/cjs/run.cjs +4 -0
  8. package/dist/cjs/run.cjs.map +1 -1
  9. package/dist/cjs/tools/SubagentTool.cjs +8 -3
  10. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  11. package/dist/cjs/tools/ToolNode.cjs +1 -1
  12. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs +399 -0
  13. package/dist/cjs/tools/subagent/InMemorySubagentTaskStore.cjs.map +1 -0
  14. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +168 -60
  15. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  16. package/dist/cjs/tools/subagent/index.cjs +1 -0
  17. package/dist/esm/graphs/Graph.mjs +32 -19
  18. package/dist/esm/graphs/Graph.mjs.map +1 -1
  19. package/dist/esm/hooks/HookRegistry.mjs +7 -1
  20. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  21. package/dist/esm/hooks/index.mjs +1 -1
  22. package/dist/esm/main.mjs +3 -2
  23. package/dist/esm/run.mjs +4 -0
  24. package/dist/esm/run.mjs.map +1 -1
  25. package/dist/esm/tools/SubagentTool.mjs +8 -3
  26. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  27. package/dist/esm/tools/ToolNode.mjs +1 -1
  28. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs +399 -0
  29. package/dist/esm/tools/subagent/InMemorySubagentTaskStore.mjs.map +1 -0
  30. package/dist/esm/tools/subagent/SubagentExecutor.mjs +168 -60
  31. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  32. package/dist/esm/tools/subagent/index.mjs +1 -0
  33. package/dist/types/graphs/Graph.d.ts +6 -3
  34. package/dist/types/hooks/HookRegistry.d.ts +8 -0
  35. package/dist/types/run.d.ts +1 -0
  36. package/dist/types/tools/SubagentTool.d.ts +3 -1
  37. package/dist/types/tools/subagent/InMemorySubagentTaskStore.d.ts +45 -0
  38. package/dist/types/tools/subagent/SubagentExecutor.d.ts +26 -3
  39. package/dist/types/tools/subagent/index.d.ts +2 -0
  40. package/dist/types/types/graph.d.ts +11 -4
  41. package/dist/types/types/index.d.ts +1 -0
  42. package/dist/types/types/run.d.ts +6 -0
  43. package/dist/types/types/subagentTasks.d.ts +140 -0
  44. package/package.json +1 -1
  45. package/src/graphs/Graph.ts +54 -30
  46. package/src/hooks/HookRegistry.ts +18 -0
  47. package/src/run.ts +4 -0
  48. package/src/tools/SubagentTool.ts +20 -2
  49. package/src/tools/subagent/InMemorySubagentTaskStore.ts +624 -0
  50. package/src/tools/subagent/SubagentExecutor.ts +341 -74
  51. package/src/tools/subagent/index.ts +2 -0
  52. package/src/types/graph.ts +11 -4
  53. package/src/types/index.ts +1 -0
  54. package/src/types/run.ts +6 -0
  55. package/src/types/subagentTasks.ts +129 -0
@@ -52,6 +52,8 @@ import type {
52
52
  ResolvedSubagentConfig,
53
53
  ResolvedSubagentConfigEntry,
54
54
  SubagentExecutionContext,
55
+ SubagentTaskConfig,
56
+ SubagentTaskRuntime,
55
57
  SubagentResolveConfigurable,
56
58
  SubagentResolveRequestContext,
57
59
  SubagentResolveUserContext,
@@ -81,12 +83,12 @@ import type {
81
83
  } from './SubagentReplay';
82
84
  import type {
83
85
  AggregatedHookResult,
84
- HookRegistry,
86
+ PostToolBatchHookOutput,
87
+ PreemptBoundaryHookOutput,
85
88
  ToolApprovalReplaySnapshot,
86
89
  } from '@/hooks';
87
90
  import type { GraphFactory } from '@/graphs/graphFactory';
88
91
  import type { StandardGraph } from '@/graphs/Graph';
89
- import type { HandlerRegistry } from '@/events';
90
92
  import {
91
93
  getSubagentApprovalExecutionScope,
92
94
  SubagentDefinitionBindingError,
@@ -101,6 +103,11 @@ import {
101
103
  SUBAGENT_RESUME_ATTEMPT_CONFIG_KEY,
102
104
  SUBAGENT_RESUME_MANIFEST_CONFIG_KEY,
103
105
  } from './SubagentReplay';
106
+ import {
107
+ executeHooks,
108
+ HookRegistry,
109
+ TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
110
+ } from '@/hooks';
104
111
  import {
105
112
  StreamLimitExceededError,
106
113
  RUN_BREAKER_SCOPE_CONFIG_KEY,
@@ -116,10 +123,6 @@ import {
116
123
  Callback,
117
124
  StepTypes,
118
125
  } from '@/common';
119
- import {
120
- executeHooks,
121
- TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY,
122
- } from '@/hooks';
123
126
  import {
124
127
  createChildGraphPlan,
125
128
  isGraphSubagentConfig,
@@ -127,7 +130,9 @@ import {
127
130
  import { stripRunStepResumeState } from '@/tools/runStepResume';
128
131
  import { seedAgentInitialSessions } from '@/utils/toolSessions';
129
132
  import { stableStringify } from '@/tools/eagerEventExecution';
133
+ import { convertInjectedMessages } from '@/messages/injected';
130
134
  import { composeAbortSignals } from '@/utils/misc';
135
+ import { HandlerRegistry } from '@/events';
131
136
 
132
137
  export {
133
138
  buildChildInputs,
@@ -140,6 +145,7 @@ export {
140
145
 
141
146
  const ERROR_MESSAGE_MAX_CHARS = 200;
142
147
  const MAX_QUEUED_SUBAGENT_UPDATES = 64;
148
+ const MAX_BACKGROUND_SUBAGENT_SEALS = 32;
143
149
  const SUBAGENT_UPDATE_HANDLER_TIMEOUT_MS = 5_000;
144
150
  const TEXT_DELTA_CONTENT_TYPE = `${ContentTypes.TEXT}_delta`;
145
151
  const SUBAGENT_RESOLUTION_ERROR_MESSAGE =
@@ -615,6 +621,22 @@ function getSettlementFingerprint(output: PersistedToolOutput): string {
615
621
  return createHash('sha256').update(stableStringify(output)).digest('hex');
616
622
  }
617
623
 
624
+ function getBackgroundTaskFingerprint(
625
+ description: string,
626
+ subagentType: string
627
+ ): string {
628
+ return createHash('sha256')
629
+ .update(stableStringify({ description, subagentType }))
630
+ .digest('hex');
631
+ }
632
+
633
+ function getBackgroundTaskHookSessionId(
634
+ sourceHookSessionId: string,
635
+ taskId: string
636
+ ): string {
637
+ return `${sourceHookSessionId}:subagent-task:${taskId}`;
638
+ }
639
+
618
640
  function deserializeToolOutput(
619
641
  output: PersistedToolOutput
620
642
  ): SettledSubagentToolOutput {
@@ -786,13 +808,26 @@ export type SubagentExecuteParams = {
786
808
  * rather than sharing parent's host context.
787
809
  */
788
810
  parentConfigurable?: Record<string, unknown>;
811
+ /** Dedicated hook session used by a detached task. @internal */
812
+ hookSessionId?: string;
813
+ /** Process-local task controls consumed by the child graph. @internal */
814
+ taskRuntime?: SubagentTaskRuntime;
789
815
  };
790
816
 
791
817
  export type SubagentExecuteResult = {
792
818
  content: string;
793
819
  messages: BaseMessage[];
820
+ /** Tagged internal failure; foreground callers retain the legacy content. */
821
+ error?: string;
794
822
  };
795
823
 
824
+ function createSubagentFailure(
825
+ content: string,
826
+ error = content
827
+ ): SubagentExecuteResult {
828
+ return { content, messages: [], error };
829
+ }
830
+
796
831
  /**
797
832
  * Factory that constructs a child graph for subagent execution. Injected
798
833
  * rather than imported so that `SubagentExecutor` does not have a runtime
@@ -848,6 +883,13 @@ export type SubagentExecutorOptions = {
848
883
  /** Preferred polymorphic child constructor. The legacy standard-only
849
884
  * factory remains required for source compatibility. */
850
885
  createChildGraphByKind?: GraphFactory;
886
+ /**
887
+ * Captures a child-graph factory and its run-scoped host dependencies
888
+ * synchronously, before a detached task can outlive parent cleanup.
889
+ */
890
+ createDetachedChildGraphFactory?: (
891
+ parentHandlerRegistry: HandlerRegistry
892
+ ) => GraphFactory;
851
893
  /**
852
894
  * Parent's event handler registry. When provided, child-graph events are
853
895
  * forwarded through this registry so hosts can:
@@ -870,6 +912,8 @@ export type SubagentExecutorOptions = {
870
912
  * nested subagents report through the same sink.
871
913
  */
872
914
  usageSink?: SubagentUsageSink;
915
+ /** Host-owned process-local task namespace for detached execution. */
916
+ taskConfig?: SubagentTaskConfig;
873
917
  };
874
918
 
875
919
  type DurableExecutionRecord = SubagentExecutionRecord<
@@ -902,7 +946,11 @@ export class SubagentExecutor {
902
946
  private readonly maxDepth: number;
903
947
  private readonly createChildGraph: ChildGraphFactory;
904
948
  private readonly createChildGraphByKind?: GraphFactory;
949
+ private readonly createDetachedChildGraphFactory?: (
950
+ parentHandlerRegistry: HandlerRegistry
951
+ ) => GraphFactory;
905
952
  private readonly usageSink?: SubagentUsageSink;
953
+ private readonly taskConfig?: SubagentTaskConfig;
906
954
  private readonly executions: SubagentExecutionRegistry<
907
955
  SubagentExecuteResult,
908
956
  ResolvedSubagentConfig,
@@ -943,7 +991,10 @@ export class SubagentExecutor {
943
991
  this.maxDepth = options.maxDepth ?? 1;
944
992
  this.createChildGraph = options.createChildGraph;
945
993
  this.createChildGraphByKind = options.createChildGraphByKind;
994
+ this.createDetachedChildGraphFactory =
995
+ options.createDetachedChildGraphFactory;
946
996
  this.usageSink = options.usageSink;
997
+ this.taskConfig = options.taskConfig;
947
998
  const rawRegistry = options.parentHandlerRegistry;
948
999
  if (typeof rawRegistry === 'function') {
949
1000
  this.resolveParentHandlerRegistry = rawRegistry;
@@ -979,6 +1030,199 @@ export class SubagentExecutor {
979
1030
  return this.resolveParentHandlerRegistry?.();
980
1031
  }
981
1032
 
1033
+ /**
1034
+ * Starts one independently-owned executor behind the configured task store.
1035
+ * The parent ToolNode receives the handle synchronously; the detached clone
1036
+ * is not registered on the parent graph, so end-of-turn cleanup cannot
1037
+ * invalidate or clear a child that intentionally outlives that turn.
1038
+ */
1039
+ executeInBackground(params: SubagentExecuteParams): string {
1040
+ if (this.taskConfig == null) {
1041
+ return JSON.stringify({
1042
+ status: 'rejected',
1043
+ message: 'Background subagent execution is not enabled for this run.',
1044
+ });
1045
+ }
1046
+ const executableConfig = this.configs.get(params.subagentType);
1047
+ if (executableConfig == null) {
1048
+ return JSON.stringify({
1049
+ status: 'rejected',
1050
+ message: `Unknown subagent type "${params.subagentType}".`,
1051
+ });
1052
+ }
1053
+ if (this.maxDepth <= 0) {
1054
+ return JSON.stringify({
1055
+ status: 'rejected',
1056
+ message: 'Maximum subagent nesting depth exceeded.',
1057
+ });
1058
+ }
1059
+ if (this.humanInTheLoop?.enabled === true) {
1060
+ return JSON.stringify({
1061
+ status: 'rejected',
1062
+ message:
1063
+ 'Background subagent execution does not support human-in-the-loop pauses.',
1064
+ });
1065
+ }
1066
+ const parentToolCallId = params.parentToolCallId?.trim();
1067
+ if (parentToolCallId == null || parentToolCallId === '') {
1068
+ return JSON.stringify({
1069
+ status: 'rejected',
1070
+ message:
1071
+ 'Background subagent execution requires a parent tool call ID.',
1072
+ });
1073
+ }
1074
+ const detachedHandlers = new HandlerRegistry();
1075
+ const sourceHookSessionId =
1076
+ asNonEmptyString(params.parentConfigurable?.run_id) ??
1077
+ this.executionContext.hookSessionId;
1078
+ const taskHookRegistry =
1079
+ this.hookRegistry?.forkSession(sourceHookSessionId) ?? new HookRegistry();
1080
+ const toolHandler = this.getParentHandlerRegistry()?.getHandler(
1081
+ GraphEvents.ON_TOOL_EXECUTE
1082
+ );
1083
+ if (toolHandler != null) {
1084
+ detachedHandlers.register(GraphEvents.ON_TOOL_EXECUTE, toolHandler);
1085
+ }
1086
+ const detachedGraphFactory =
1087
+ this.createDetachedChildGraphFactory?.(detachedHandlers);
1088
+ const started = this.taskConfig.store.start({
1089
+ scopeId: this.taskConfig.scopeId,
1090
+ idempotencyKey: JSON.stringify([
1091
+ this.parentRunId,
1092
+ this.parentAgentId ?? '',
1093
+ parentToolCallId,
1094
+ ]),
1095
+ requestFingerprint: getBackgroundTaskFingerprint(
1096
+ params.description,
1097
+ params.subagentType
1098
+ ),
1099
+ subagentType: params.subagentType,
1100
+ run: (runtime) =>
1101
+ this.executeDetached(
1102
+ params,
1103
+ runtime,
1104
+ detachedHandlers,
1105
+ taskHookRegistry,
1106
+ detachedGraphFactory
1107
+ ),
1108
+ });
1109
+ if (!started.accepted) {
1110
+ if (started.reason === 'conflict') {
1111
+ return JSON.stringify({
1112
+ status: 'rejected',
1113
+ tool: Constants.SUBAGENT,
1114
+ message:
1115
+ 'The same parent tool call ID was already used with different background subagent arguments.',
1116
+ });
1117
+ }
1118
+ return JSON.stringify({
1119
+ status: 'rejected',
1120
+ tool: Constants.SUBAGENT,
1121
+ message:
1122
+ '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
+ });
1124
+ }
1125
+ return JSON.stringify({
1126
+ background_task_id: started.task.taskId,
1127
+ tool: Constants.SUBAGENT,
1128
+ subagent_type: params.subagentType,
1129
+ status: started.task.status,
1130
+ message: `${started.isNew ? 'Started' : 'Reused'} subagent "${params.subagentType}" background task. Poll the host background-task tool with background_task_id "${started.task.taskId}" to check progress or collect its result.`,
1131
+ });
1132
+ }
1133
+
1134
+ private async executeDetached(
1135
+ params: SubagentExecuteParams,
1136
+ runtime: SubagentTaskRuntime,
1137
+ detachedHandlers: HandlerRegistry,
1138
+ taskHookRegistry: HookRegistry,
1139
+ detachedGraphFactory?: GraphFactory
1140
+ ): Promise<SubagentExecuteResult> {
1141
+ const sourceHookSessionId =
1142
+ asNonEmptyString(params.parentConfigurable?.run_id) ??
1143
+ this.executionContext.hookSessionId;
1144
+ const taskHookSessionId = getBackgroundTaskHookSessionId(
1145
+ sourceHookSessionId,
1146
+ runtime.taskId
1147
+ );
1148
+ const unregisterHooks: Array<() => void> = [];
1149
+ unregisterHooks.push(
1150
+ taskHookRegistry.registerSession(taskHookSessionId, 'PostToolBatch', {
1151
+ hooks: [
1152
+ (): PostToolBatchHookOutput => ({
1153
+ injectedMessages: runtime.drain('tool'),
1154
+ }),
1155
+ ],
1156
+ }),
1157
+ taskHookRegistry.registerSession(taskHookSessionId, 'PreemptBoundary', {
1158
+ hooks: [
1159
+ (): PreemptBoundaryHookOutput => ({
1160
+ injectedMessages: runtime.drain('preempt'),
1161
+ }),
1162
+ ],
1163
+ })
1164
+ );
1165
+
1166
+ detachedHandlers.register(GraphEvents.ON_SUBAGENT_UPDATE, {
1167
+ handle: (_event, data): void => {
1168
+ runtime.reportProgress(data as SubagentUpdateEvent);
1169
+ },
1170
+ });
1171
+
1172
+ const detached = new SubagentExecutor({
1173
+ configs: this.configs,
1174
+ parentSignal: runtime.signal,
1175
+ hookRegistry: taskHookRegistry,
1176
+ parentHandlerRegistry: detachedHandlers,
1177
+ parentRunId: this.parentRunId,
1178
+ parentAgentId: this.parentAgentId,
1179
+ executionContext: {
1180
+ ...this.executionContext,
1181
+ hookSessionId: taskHookSessionId,
1182
+ },
1183
+ langfuse: this.langfuse,
1184
+ tokenCounter: this.tokenCounter,
1185
+ usageSink: this.usageSink,
1186
+ streamLimits: this.streamLimits,
1187
+ maxDepth: this.maxDepth,
1188
+ createChildGraph:
1189
+ detachedGraphFactory == null
1190
+ ? this.createChildGraph
1191
+ : (input): StandardGraph =>
1192
+ detachedGraphFactory({ kind: 'standard', input }),
1193
+ createChildGraphByKind:
1194
+ detachedGraphFactory ?? this.createChildGraphByKind,
1195
+ });
1196
+ try {
1197
+ const result = await detached.execute({
1198
+ ...params,
1199
+ signal: undefined,
1200
+ breaker: undefined,
1201
+ hookSessionId: taskHookSessionId,
1202
+ taskRuntime: runtime,
1203
+ parentConfigurable: {
1204
+ ...params.parentConfigurable,
1205
+ run_id: taskHookSessionId,
1206
+ },
1207
+ });
1208
+ if (runtime.signal.aborted) {
1209
+ throw runtime.signal.reason instanceof Error
1210
+ ? runtime.signal.reason
1211
+ : new Error('Detached subagent task cancelled.');
1212
+ }
1213
+ if (result.error != null) {
1214
+ throw new Error(result.error);
1215
+ }
1216
+ return result;
1217
+ } finally {
1218
+ detached.clearHeavyState();
1219
+ for (const unregister of unregisterHooks) {
1220
+ unregister();
1221
+ }
1222
+ taskHookRegistry.clearSession(taskHookSessionId);
1223
+ }
1224
+ }
1225
+
982
1226
  private bindExecutionDefinition(
983
1227
  execution: DurableExecutionRecord,
984
1228
  binding: SubagentDefinitionBinding,
@@ -1940,36 +2184,38 @@ export class SubagentExecutor {
1940
2184
  const executableConfig = this.configs.get(params.subagentType);
1941
2185
  if (executableConfig == null) {
1942
2186
  const available = [...this.configs.keys()].join(', ');
1943
- return Promise.resolve({
1944
- content: `Error: Unknown subagent type "${params.subagentType}". Available types: ${available}`,
1945
- messages: [],
1946
- });
2187
+ return Promise.resolve(
2188
+ createSubagentFailure(
2189
+ `Error: Unknown subagent type "${params.subagentType}". Available types: ${available}`
2190
+ )
2191
+ );
1947
2192
  }
1948
2193
  if (this.maxDepth <= 0) {
1949
- return Promise.resolve({
1950
- content: 'Error: Maximum subagent nesting depth exceeded.',
1951
- messages: [],
1952
- });
2194
+ return Promise.resolve(
2195
+ createSubagentFailure(
2196
+ 'Error: Maximum subagent nesting depth exceeded.'
2197
+ )
2198
+ );
1953
2199
  }
1954
2200
  if (
1955
2201
  isGraphSubagentConfig(executableConfig) &&
1956
2202
  this.humanInTheLoop?.enabled === true
1957
2203
  ) {
1958
- return Promise.resolve({
1959
- content:
1960
- 'Error: Human-in-the-loop execution is not yet supported for graph subagents.',
1961
- messages: [],
1962
- });
2204
+ return Promise.resolve(
2205
+ createSubagentFailure(
2206
+ 'Error: Human-in-the-loop execution is not yet supported for graph subagents.'
2207
+ )
2208
+ );
1963
2209
  }
1964
2210
  if (
1965
2211
  this.humanInTheLoop?.enabled === true &&
1966
2212
  (params.parentToolCallId == null || params.parentToolCallId === '')
1967
2213
  ) {
1968
- return Promise.resolve({
1969
- content:
1970
- 'Error: Resumable subagent execution requires a parent tool call ID.',
1971
- messages: [],
1972
- });
2214
+ return Promise.resolve(
2215
+ createSubagentFailure(
2216
+ 'Error: Resumable subagent execution requires a parent tool call ID.'
2217
+ )
2218
+ );
1973
2219
  }
1974
2220
  const execution = this.executions.open({
1975
2221
  threadId: params.threadId,
@@ -1989,16 +2235,14 @@ export class SubagentExecutor {
1989
2235
  );
1990
2236
  } catch (error) {
1991
2237
  if (error instanceof SubagentDefinitionBindingError) {
1992
- return Promise.resolve({
1993
- content: SUBAGENT_CONFIG_CHANGED_MESSAGE,
1994
- messages: [],
1995
- });
2238
+ return Promise.resolve(
2239
+ createSubagentFailure(SUBAGENT_CONFIG_CHANGED_MESSAGE)
2240
+ );
1996
2241
  }
1997
2242
  if (error instanceof SubagentInvocationBindingError) {
1998
- return Promise.resolve({
1999
- content: SUBAGENT_INVOCATION_CHANGED_MESSAGE,
2000
- messages: [],
2001
- });
2243
+ return Promise.resolve(
2244
+ createSubagentFailure(SUBAGENT_INVOCATION_CHANGED_MESSAGE)
2245
+ );
2002
2246
  }
2003
2247
  throw error;
2004
2248
  }
@@ -2030,10 +2274,7 @@ export class SubagentExecutor {
2030
2274
  )
2031
2275
  ) {
2032
2276
  this.executions.remove(execution);
2033
- return {
2034
- content: SUBAGENT_CONFIG_CHANGED_MESSAGE,
2035
- messages: [],
2036
- };
2277
+ return createSubagentFailure(SUBAGENT_CONFIG_CHANGED_MESSAGE);
2037
2278
  }
2038
2279
  let identity: SubagentExecutionIdentity;
2039
2280
  try {
@@ -2043,10 +2284,7 @@ export class SubagentExecutor {
2043
2284
  if (error instanceof StreamLimitExceededError) {
2044
2285
  throw error;
2045
2286
  }
2046
- return {
2047
- content: SUBAGENT_RESOLUTION_ERROR_MESSAGE,
2048
- messages: [],
2049
- };
2287
+ return createSubagentFailure(SUBAGENT_RESOLUTION_ERROR_MESSAGE);
2050
2288
  }
2051
2289
  const { childRunId, childThreadId, approvalExecutionScope } = identity;
2052
2290
  const bound = this.bindExecutionDefinition(
@@ -2060,7 +2298,7 @@ export class SubagentExecutor {
2060
2298
  'effective'
2061
2299
  );
2062
2300
  if (!bound) {
2063
- return { content: SUBAGENT_CONFIG_CHANGED_MESSAGE, messages: [] };
2301
+ return createSubagentFailure(SUBAGENT_CONFIG_CHANGED_MESSAGE);
2064
2302
  }
2065
2303
  const completedChildResult = execution.completedResult;
2066
2304
  if (completedChildResult != null) {
@@ -2081,10 +2319,7 @@ export class SubagentExecutor {
2081
2319
  if (error instanceof StreamLimitExceededError) {
2082
2320
  throw error;
2083
2321
  }
2084
- return {
2085
- content: SUBAGENT_RESOLUTION_ERROR_MESSAGE,
2086
- messages: [],
2087
- };
2322
+ return createSubagentFailure(SUBAGENT_RESOLUTION_ERROR_MESSAGE);
2088
2323
  }
2089
2324
 
2090
2325
  const parentRegistry = this.getParentHandlerRegistry();
@@ -2109,6 +2344,7 @@ export class SubagentExecutor {
2109
2344
  });
2110
2345
  const childAgentId = childPlan.subjectAgentId;
2111
2346
  const currentHookSessionId =
2347
+ asNonEmptyString(params.hookSessionId) ??
2112
2348
  asNonEmptyString(params.parentConfigurable?.run_id) ??
2113
2349
  this.executionContext.hookSessionId;
2114
2350
  const childExecutionContext: SubagentExecutionContext = {
@@ -2132,7 +2368,8 @@ export class SubagentExecutor {
2132
2368
  const memberRecursionLimit = maxTurns * SUBAGENT_RECURSION_MULTIPLIER;
2133
2369
  const recursionLimit =
2134
2370
  memberRecursionLimit *
2135
- (childPlan.kind === 'graph' ? childPlan.agents.length : 1);
2371
+ (childPlan.kind === 'graph' ? childPlan.agents.length : 1) +
2372
+ (params.taskRuntime == null ? 0 : MAX_BACKGROUND_SUBAGENT_SEALS);
2136
2373
 
2137
2374
  const hostUsageSink = this.usageSink;
2138
2375
  let subagentUsageSink: SubagentUsageSink | undefined;
@@ -2162,15 +2399,22 @@ export class SubagentExecutor {
2162
2399
  * never see grandchild model calls.
2163
2400
  */
2164
2401
  subagentUsageSink,
2402
+ ...(params.taskRuntime == null
2403
+ ? {}
2404
+ : {
2405
+ preemption: {
2406
+ shouldPreempt: (): boolean =>
2407
+ params.taskRuntime?.shouldPreempt() === true,
2408
+ maxSeals: MAX_BACKGROUND_SUBAGENT_SEALS,
2409
+ },
2410
+ }),
2165
2411
  };
2166
2412
  let childGraph = cachedChildRun?.graph;
2167
2413
  if (childGraph == null && childPlan.kind === 'graph') {
2168
2414
  if (this.createChildGraphByKind == null) {
2169
- return {
2170
- content:
2171
- 'Error: Graph subagent execution requires a polymorphic child graph factory.',
2172
- messages: [],
2173
- };
2415
+ return createSubagentFailure(
2416
+ 'Error: Graph subagent execution requires a polymorphic child graph factory.'
2417
+ );
2174
2418
  }
2175
2419
  childGraph = this.createChildGraphByKind({
2176
2420
  kind: 'multi-agent',
@@ -2183,6 +2427,9 @@ export class SubagentExecutor {
2183
2427
  });
2184
2428
  }
2185
2429
  childGraph ??= this.createChildGraph(childGraphInput);
2430
+ if (params.taskRuntime != null) {
2431
+ childGraph.hookRegistry = this.hookRegistry;
2432
+ }
2186
2433
  if (cachedChildRun == null) {
2187
2434
  seedChildGraphSessions(childGraph, childPlan.agents);
2188
2435
  }
@@ -2426,26 +2673,46 @@ export class SubagentExecutor {
2426
2673
  });
2427
2674
  }
2428
2675
 
2429
- let childResult: MultiAgentGraphState;
2430
- if (this.humanInTheLoop?.enabled === true) {
2431
- /** Execute as an independently checkpointed root instead of inheriting
2432
- * the parent's Pregel namespace. Parent decisions are routed explicitly
2433
- * by interrupt id, so concurrent children keep isolated resume state. */
2434
- childResult = await AsyncLocalStorageProviderSingleton.runWithConfig(
2435
- childInvokeConfig,
2436
- (): Promise<MultiAgentGraphState> =>
2437
- workflow.invoke(childInput, childInvokeConfig)
2438
- );
2439
- } else {
2440
- childResult = await workflow.invoke(childInput, childInvokeConfig);
2441
- }
2442
- const childInterrupts = isInterrupted(childResult)
2443
- ? childResult[INTERRUPT]
2444
- : undefined;
2445
- if (childInterrupts != null && childInterrupts.length > 0) {
2446
- throw new GraphInterrupt(childInterrupts);
2676
+ for (;;) {
2677
+ let childResult: MultiAgentGraphState;
2678
+ if (this.humanInTheLoop?.enabled === true) {
2679
+ /** Execute as an independently checkpointed root instead of inheriting
2680
+ * the parent's Pregel namespace. Parent decisions are routed explicitly
2681
+ * by interrupt id, so concurrent children keep isolated resume state. */
2682
+ childResult =
2683
+ await AsyncLocalStorageProviderSingleton.runWithConfig(
2684
+ childInvokeConfig,
2685
+ (): Promise<MultiAgentGraphState> =>
2686
+ workflow.invoke(childInput, childInvokeConfig)
2687
+ );
2688
+ } else {
2689
+ childResult = await workflow.invoke(childInput, childInvokeConfig);
2690
+ }
2691
+ const childInterrupts = isInterrupted(childResult)
2692
+ ? childResult[INTERRUPT]
2693
+ : undefined;
2694
+ if (childInterrupts != null && childInterrupts.length > 0) {
2695
+ throw new GraphInterrupt(childInterrupts);
2696
+ }
2697
+ result = childResult;
2698
+ const continuation = params.taskRuntime?.closeTurn();
2699
+ if (continuation == null || continuation.closed) {
2700
+ break;
2701
+ }
2702
+ /**
2703
+ * A queued control becomes the next user turn inside the same
2704
+ * detached task. Reset graph sidecars while carrying the complete
2705
+ * child transcript forward, preserving one task/trace identity and
2706
+ * avoiding a second subagent lifecycle or duplicate billing path.
2707
+ */
2708
+ childGraph.resetValues(true);
2709
+ childInput = {
2710
+ messages: [
2711
+ ...childResult.messages,
2712
+ ...convertInjectedMessages(continuation.messages),
2713
+ ],
2714
+ };
2447
2715
  }
2448
- result = childResult;
2449
2716
  }
2450
2717
  } catch (error) {
2451
2718
  /** Stamped at failure, not after the error-envelope work below. */
@@ -2535,10 +2802,10 @@ export class SubagentExecutor {
2535
2802
  if (error instanceof StreamLimitExceededError) {
2536
2803
  throw error;
2537
2804
  }
2538
- return {
2539
- content: `Subagent error: ${errorMessage}`,
2540
- messages: [],
2541
- };
2805
+ return createSubagentFailure(
2806
+ `Subagent error: ${errorMessage}`,
2807
+ errorMessage
2808
+ );
2542
2809
  }
2543
2810
 
2544
2811
  /**
@@ -17,3 +17,5 @@ export type {
17
17
  SubagentExecutorOptions,
18
18
  ChildGraphFactory,
19
19
  } from './SubagentExecutor';
20
+ export { InMemorySubagentTaskStore } from './InMemorySubagentTaskStore';
21
+ export type { InMemorySubagentTaskStoreOptions } from './InMemorySubagentTaskStore';
@@ -40,6 +40,7 @@ import type {
40
40
  TokenBudgetBreakdown,
41
41
  } from '@/types/run';
42
42
  import type { Providers, Callback, GraphNodeKeys } from '@/common';
43
+ import type { SubagentTaskConfig } from '@/types/subagentTasks';
43
44
  import type { StandardGraph, MultiAgentGraph } from '@/graphs';
44
45
  import type { ClientOptions } from '@/types/llm';
45
46
 
@@ -352,6 +353,12 @@ export type StandardGraphInput = {
352
353
  * they already flow through the registry's `CHAT_MODEL_END` handler.
353
354
  */
354
355
  subagentUsageSink?: SubagentUsageSink;
356
+ /**
357
+ * Optional host-owned process-local task namespace for detached subagents.
358
+ * Presence enables `run_in_background` on the subagent tool. Child graphs
359
+ * do not inherit it, keeping background nesting disabled for the MVP.
360
+ */
361
+ subagentTasks?: SubagentTaskConfig;
355
362
  /**
356
363
  * True when this graph IS a subagent child run (set by `SubagentExecutor`
357
364
  * when it constructs the child graph). Drives the hook-input `agentId`
@@ -363,10 +370,10 @@ export type StandardGraphInput = {
363
370
  */
364
371
  subagentScope?: boolean;
365
372
  /**
366
- * Cooperative preemption, forwarded from `RunConfig.preemption`. Only ever
367
- * set on the top-level graph: a steer targets the conversation, so subagent
368
- * children must run to completion and `buildChildInputs` does not propagate
369
- * this field.
373
+ * Cooperative preemption, forwarded from `RunConfig.preemption`. Ordinary
374
+ * child graphs do not inherit it. Detached subagent tasks may receive their
375
+ * own internal parent-control source so an interrupt can reuse the same
376
+ * provider-safe sealing path without targeting the top-level conversation.
370
377
  */
371
378
  preemption?: StreamPreemption;
372
379
  /**
@@ -6,6 +6,7 @@ export * from './messages';
6
6
  export * from './run';
7
7
  export * from './skill';
8
8
  export * from './stream';
9
+ export * from './subagentTasks';
9
10
  export * from './tools';
10
11
  export * from './summarize';
11
12
  export * from './activityLabel';
package/src/types/run.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  ToolOutputReferencesConfig,
11
11
  EagerEventToolExecutionConfig,
12
12
  } from '@/types/tools';
13
+ import type { SubagentTaskConfig } from '@/types/subagentTasks';
13
14
  import type { HumanInTheLoopConfig } from '@/types/hitl';
14
15
  import type { HookRegistry } from '@/hooks';
15
16
  import type * as s from '@/types/stream';
@@ -234,6 +235,11 @@ export type RunConfig = {
234
235
  * the registered `CHAT_MODEL_END` handler as usual.
235
236
  */
236
237
  subagentUsageSink?: g.SubagentUsageSink;
238
+ /**
239
+ * Trusted process-local task namespace for detached subagent execution.
240
+ * Omit to preserve foreground-only subagent behavior.
241
+ */
242
+ subagentTasks?: SubagentTaskConfig;
237
243
  /**
238
244
  * Pre-constructed hook registry for this run. Hooks fire at lifecycle
239
245
  * points in `processStream` (RunStart, UserPromptSubmit, Stop,