@@ -193,6 +193,36 @@ internal val DEFAULT_PROJECT_NAME: String? by lazy {
193193
194194internal val DEFAULT_EXECUTOR : ExecutorService by lazy { Executors .newCachedThreadPool() }
195195
196+ /* *
197+ * Controls how the parent [RunTree] is resolved for a traced run.
198+ * - [AUTO] (default) — resolve the parent from the current thread's context.
199+ * - [of]`(parent)` — use the given [RunTree] as the parent, regardless of thread context.
200+ * - [none]`()` — force a new root trace, ignoring any active thread context.
201+ *
202+ * ```java
203+ * TraceConfig.builder().parent(parent).build(); // child of parent
204+ * TraceConfig.builder().parent(null).build(); // new root trace
205+ * // .parent() not called // auto-resolve from thread
206+ * ```
207+ */
208+ class ParentConfig
209+ private constructor (internal val runTree: RunTree ? , internal val isExplicit: Boolean ) {
210+ companion object {
211+ /* * Resolve the parent from the current thread's context (default). */
212+ @JvmField val AUTO = ParentConfig (runTree = null , isExplicit = false )
213+
214+ /* * Force a new root trace — no parent, even if one exists on the thread. */
215+ @JvmField val NONE = ParentConfig (runTree = null , isExplicit = true )
216+
217+ /* * Force a new root trace — no parent, even if one exists on the thread. */
218+ @JvmStatic fun none (): ParentConfig = NONE
219+
220+ /* * Use the given [RunTree] as the explicit parent. */
221+ @JvmStatic
222+ fun of (parent : RunTree ): ParentConfig = ParentConfig (runTree = parent, isExplicit = true )
223+ }
224+ }
225+
196226/* *
197227 * Configuration for a traced run.
198228 *
@@ -254,6 +284,38 @@ class TraceConfig(
254284 * @see TraceProcessIO
255285 */
256286 val processTracedIO : TraceProcessIO <* , * >? = null ,
287+ /* *
288+ * Explicit parent [RunTree] override.
289+ *
290+ * By default the parent is resolved from the current thread's context. Set this to control
291+ * parent linkage explicitly:
292+ * - **[ParentConfig.of]`(parent)`** — attach this run as a child of `parent`, even on a thread
293+ * with no context (e.g. a framework-managed thread pool).
294+ * - **[ParentConfig.none]`()`** — force this run to be a new root trace, even when a parent
295+ * exists on the current thread.
296+ *
297+ * ```java
298+ * // Capture the parent on the originating thread
299+ * RunTree parent = Tracing.getCurrentRunTree();
300+ *
301+ * // Force a child relationship on a worker thread
302+ * TraceConfig child = TraceConfig.builder()
303+ * .name("my-tool")
304+ * .parent(parent) // attach as child
305+ * .build();
306+ *
307+ * // Force a new root trace
308+ * TraceConfig root = TraceConfig.builder()
309+ * .name("independent-trace")
310+ * .parent(null) // new root, ignores thread-local
311+ * .build();
312+ * ```
313+ *
314+ * @see ParentConfig
315+ * @see getCurrentRunTree
316+ * @see withParent
317+ */
318+ val parent : ParentConfig = ParentConfig .AUTO ,
257319) {
258320 companion object {
259321 /* * Creates a new [Builder] for constructing a [TraceConfig]. */
@@ -284,6 +346,7 @@ class TraceConfig(
284346 private var executor: ExecutorService ? = null
285347 private var tracingEnabled: Boolean? = null
286348 private var processTracedIO: TraceProcessIO <* , * >? = null
349+ private var parent: ParentConfig = ParentConfig .AUTO
287350
288351 @JvmSynthetic
289352 internal fun from (config : TraceConfig ) = apply {
@@ -296,6 +359,7 @@ class TraceConfig(
296359 executor = config.executor
297360 tracingEnabled = config.tracingEnabled
298361 processTracedIO = config.processTracedIO
362+ parent = config.parent
299363 }
300364
301365 /* * The name of the run, displayed in LangSmith. */
@@ -332,6 +396,19 @@ class TraceConfig(
332396 this .processTracedIO = processTracedIO
333397 }
334398
399+ /* *
400+ * Sets the parent [RunTree] for this run.
401+ *
402+ * Pass a [RunTree] to attach this run as a child — useful for cross-thread context
403+ * propagation. Pass `null` to force a new root trace, ignoring any parent that may be
404+ * active on the current thread.
405+ *
406+ * @see TraceConfig.parent
407+ */
408+ fun parent (parent : RunTree ? ) = apply {
409+ this .parent = if (parent != null ) ParentConfig .of(parent) else ParentConfig .NONE
410+ }
411+
335412 /* * Builds the [TraceConfig]. */
336413 fun build () =
337414 TraceConfig (
@@ -344,6 +421,7 @@ class TraceConfig(
344421 executor = executor,
345422 tracingEnabled = tracingEnabled,
346423 processTracedIO = processTracedIO,
424+ parent = parent,
347425 )
348426 }
349427}
@@ -632,7 +710,11 @@ private fun toStringKeyedMap(value: Any?): Map<String, Any?>? {
632710}
633711
634712private fun <T > executeTraced (config : TraceConfig , inputs : Map <String , Any ?>? , block : () -> T ): T {
635- val parentRun = CURRENT_RUN .get()
713+ // Resolve parent: explicit config.parent always wins over thread-local.
714+ // AUTO (default) → resolve from thread-local
715+ // ParentConfig.of(run) → use that run as parent
716+ // ParentConfig.none() → force new root trace (no parent)
717+ val parentRun = if (config.parent.isExplicit) config.parent.runTree else CURRENT_RUN .get()
636718
637719 // Merge config: child config wins, then parent, then defaults.
638720 val tracingEnabled = config.tracingEnabled ? : parentRun?.tracingEnabled ? : isTracingEnabled()
0 commit comments