Skip to content

Commit a1409fb

Browse files
authored
feat: Allow manually specifying parent run tree in traceable config (#126)
* Allow manually specifying parent run tree in traceable config * Allow passing parent(null) * Agents.md fixes * Use singleton in both places
1 parent 428c872 commit a1409fb

5 files changed

Lines changed: 742 additions & 94 deletions

File tree

gradlew.bat

Lines changed: 93 additions & 93 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

langsmith-java-core/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,8 @@ dependencies {
9696
testImplementation("org.mockito:mockito-core:5.14.2")
9797
testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
9898
testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
99+
100+
// LangChain4j for testing parallel tool execution context propagation
101+
testImplementation("dev.langchain4j:langchain4j:1.12.2")
102+
testImplementation("dev.langchain4j:langchain4j-core:1.12.2")
99103
}

langsmith-java-core/src/main/kotlin/com/langchain/smith/tracing/Traceable.kt

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,36 @@ internal val DEFAULT_PROJECT_NAME: String? by lazy {
193193

194194
internal 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

634712
private 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

Comments
 (0)