Skip to content

feat: Add traceable function wrapper for LangSmith tracing#101

Merged
Jacob Lee (jacoblee93) merged 18 commits intonextfrom
jacob/traceable
Mar 26, 2026
Merged

feat: Add traceable function wrapper for LangSmith tracing#101
Jacob Lee (jacoblee93) merged 18 commits intonextfrom
jacob/traceable

Conversation

@jacoblee93
Copy link
Copy Markdown
Collaborator

@jacoblee93 Jacob Lee (jacoblee93) commented Mar 25, 2026

Adds a traceable() API that wraps functions with LangSmith tracing — the Java/Kotlin equivalent of the @traceable decorator in the Python/JS SDKs.

Usage

Kotlin

import com.langchain.smith.client.okhttp.LangsmithOkHttpClient
import com.langchain.smith.tracing.traceable
import com.langchain.smith.tracing.TraceConfig
import com.langchain.smith.tracing.RunType

val client = LangsmithOkHttpClient.fromEnv()

// Wrap a function — name is inferred from the reference
fun answerQuestion(question: String): String = "42"
val traced = traceable(::answerQuestion, TraceConfig(client = client))
val result = traced("What is the meaning of life?")

// Or with explicit config
val tracedTool = traceable(
    { input: String -> "result for $input" },
    TraceConfig("my-tool", client = client, runType = RunType.TOOL, metadata = mapOf("version" to 1)),
)

Java

import com.langchain.smith.client.LangsmithClient;
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient;
import com.langchain.smith.tracing.Tracing;
import com.langchain.smith.tracing.TraceConfig;

import java.util.function.Function;

LangsmithClient client = LangsmithOkHttpClient.fromEnv();

Function<String, String> traced = Tracing.traceable(
    (Function<String, String>) q -> "42",
    TraceConfig.builder()
        .name("answer-question")
        .client(client)
        .build());
String result = traced.apply("What is the meaning of life?");

Nested traces

Traced functions automatically nest — calling a traced function inside another creates a parent-child relationship in LangSmith. Child runs inherit client, projectName, executor, and tracingEnabled from their parent automatically.

Kotlin
val client = LangsmithOkHttpClient.fromEnv()

// Root — must provide client
val callLLM = traceable(
    { question: String ->
        openai.chat().completions().create(/* ... */)
    },
    TraceConfig("call-llm", client = client, runType = RunType.LLM),
)

// Children — no client needed, inherited from parent
val formatAnswer = traceable(
    { answer: String -> "The answer is: $answer" },
    TraceConfig("format-answer"),
)

val agent = traceable(
    { input: Map<String, Any?> ->
        val answer = callLLM(input["question"] as String)
        formatAnswer(answer)
    },
    TraceConfig("my-agent", client = client),
)

agent(mapOf("question" to "What is 2+2?"))
Java
LangsmithClient client = LangsmithOkHttpClient.fromEnv();

// Root — must provide client
Function<String, String> callLLM = Tracing.traceable(
    (Function<String, String>) question -> { /* call OpenAI */ return "4"; },
    TraceConfig.builder().name("call-llm").client(client).runType(RunType.LLM).build());

// Children — no client needed, inherited from parent
Function<String, String> formatAnswer = Tracing.traceable(
    (Function<String, String>) answer -> "The answer is: " + answer,
    TraceConfig.builder().name("format-answer").build());

Function<Map<String, Object>, String> agent = Tracing.traceable(
    (Function<Map<String, Object>, String>) input -> {
        String answer = callLLM.apply((String) input.get("question"));
        return formatAnswer.apply(answer);
    },
    TraceConfig.builder().name("my-agent").client(client).build());

agent.apply(Map.of("question", "What is 2+2?"));

Accessing and mutating the current run

Use getCurrentRunTree() inside a traced function to access the RunTree and attach metadata or extra data at runtime:

Kotlin
val traced = traceable({ input: String ->
    val run = getCurrentRunTree()!!
    run.metadata["custom_key"] = "custom_value"
    run.extra["usage"] = mapOf("tokens" to 42)
    "result"
}, TraceConfig("my-run", client = client))
Java
Function<String, String> traced = Tracing.traceable(
    (Function<String, String>) input -> {
        RunTree run = Tracing.getCurrentRunTree();
        run.getMetadata().put("custom_key", "custom_value");
        run.getExtra().put("usage", Map.of("tokens", 42));
        return "result";
    },
    TraceConfig.builder().name("my-run").client(client).build());

Creating RunTrees manually

RunTree can be constructed directly for custom integrations:

Kotlin
val run = RunTree(name = "my-run", runType = RunType.LLM, client = myClient)
val child = run.createChild(TraceConfig("child-step"))
child.inputs = mapOf("question" to "hello")
Java
RunTree run = RunTree.builder()
    .name("my-run")
    .runType(RunType.LLM)
    .client(client)
    .build();
RunTree child = run.createChild(TraceConfig.builder().name("child-step").build());
child.setInputs(Map.of("question", "hello"));

Default client

If LangsmithOkHttpClient is on the classpath and LANGSMITH_API_KEY is set, a default client is created automatically — no explicit client needed:

// Just works if LANGSMITH_API_KEY is set
val traced = traceable({ input: String -> "result" }, TraceConfig("my-func"))

Or set a default client explicitly:

setDefaultClient(LangsmithOkHttpClient.fromEnv())

Client resolution order: config → parent run → setDefaultClient() → auto-created from env → error.

Reusing configs

TraceConfig is immutable and safe to reuse. Create a base config and derive per-run configs with toBuilder():

Kotlin
val base = TraceConfig.builder().client(client).projectName("my-project").build()
val step1 = traceable({ ... }, base.toBuilder().name("step-1").build())
val step2 = traceable({ ... }, base.toBuilder().name("step-2").runType(RunType.LLM).build())
Java
TraceConfig base = TraceConfig.builder()
    .client(client)
    .projectName("my-project")
    .build();

var step1 = Tracing.traceable(..., base.toBuilder().name("step-1").build());
var step2 = Tracing.traceable(..., base.toBuilder().name("step-2").runType(RunType.LLM).build());

Async context propagation

Run context propagates automatically for synchronous nested calls. On Java 21+, ScopedValue is used, which also propagates into StructuredTaskScope child tasks. On older JVMs, ThreadLocal is used as a fallback.

Neither mechanism automatically propagates context across unstructured async boundaries such as CompletableFuture.supplyAsync, ExecutorService.submit, or Kotlin coroutines. For those cases, use getCurrentRunTree() + withParent():

Kotlin
val parent = getCurrentRunTree()
CompletableFuture.supplyAsync {
    withParent(parent) {
        tracedChild("input")
    }
}
Java
RunTree parent = Tracing.getCurrentRunTree();
CompletableFuture.supplyAsync(() ->
    Tracing.withParent(parent, () -> tracedChild.apply("input")));

Enabling tracing

Tracing is controlled by environment variables (matching the Python/JS SDKs):

export LANGSMITH_TRACING=true
# or: LANGSMITH_TRACING_V2, LANGCHAIN_TRACING, LANGCHAIN_TRACING_V2

Or per-config:

val traced = traceable({ "result" }, TraceConfig("my-func", tracingEnabled = true))

When disabled, traceable wrappers still execute the underlying function but skip all LangSmith communication (zero overhead).

What's included

  • traceable() — top-level function overloads for 0/1/2/3-arg functions, with dual Kotlin lambda and Java functional interface (Supplier/Function/BiFunction/TriFunction) variants. From Java: Tracing.traceable(...).
  • RunTree — public, mutable representation of a run. Constructable manually (Kotlin named args or Java builder). Methods: createChild(), postRun(), patchRun(), buildRunData().
  • TraceConfig — configuration for a traced run (name, client, type, metadata, tags, project, executor, enabled). Kotlin named args constructor + Java Builder. Reusable via toBuilder().
  • RunType — extensible run type with predefined constants (CHAIN, LLM, TOOL, RETRIEVER) and RunType.of("custom") for arbitrary values.
  • getCurrentRunTree() — returns the current RunTree during execution. From Java: Tracing.getCurrentRunTree().
  • setDefaultClient() — sets the global default client. From Java: Tracing.setDefaultClient(...).
  • withParent() — manual context propagation for async boundaries. From Java: Tracing.withParent(...).
  • RunContext — internal abstraction over ScopedValue (Java 21+) with ThreadLocal fallback.
  • isTracingEnabled() — checks LANGSMITH_TRACING / LANGCHAIN_TRACING env vars and system properties.

Known limitations (v0)

  • No streaming support — only synchronous function wrapping.
  • No batching — each run posts/patches individually via ingestBatch. TODO to delegate to client-level background processing.
  • 4+ arg functions — only up to 3 args supported.
  • Non-map inputs/outputs — typed SDK objects (e.g. ChatCompletionCreateParams) fall back to toString(). Pass Map inputs/outputs for clean traces. processInputs/processOutputs callbacks are planned.
  • Flush/shutdown — will be handled at the client level in a future release.
  • OpenAI tool calling integration test — deferred until OpenAI SDK dependency is bumped (requires updating OTel wrappers for new abstract members).

@jacoblee93 Jacob Lee (jacoblee93) changed the base branch from main to next March 25, 2026 20:40
@jacoblee93 Jacob Lee (jacoblee93) changed the title Jacob/traceable feat: Add traceable function wrapper for LangSmith tracing Mar 26, 2026
@jacoblee93 Jacob Lee (jacoblee93) marked this pull request as ready for review March 26, 2026 03:17
Copy link
Copy Markdown

@rx5ad Rafid S (rx5ad) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Comment thread langsmith-java-core/src/main/kotlin/com/langchain/smith/tracing/Traceable.kt Outdated
Comment thread langsmith-java-core/src/main/kotlin/com/langchain/smith/tracing/Traceable.kt Outdated
Comment thread langsmith-java-core/src/test/kotlin/com/langchain/smith/tracing/Uuidv7Test.kt Outdated
@jacoblee93 Jacob Lee (jacoblee93) merged commit f3bf340 into next Mar 26, 2026
8 checks passed
@jacoblee93 Jacob Lee (jacoblee93) deleted the jacob/traceable branch March 26, 2026 20:53
@stainless-app stainless-app bot mentioned this pull request Mar 26, 2026
Jacob Lee (jacoblee93) added a commit that referenced this pull request Mar 31, 2026
* Revert "main to next (#108)" (#110)

This reverts commit 5a8988d.

* Fix/lint and remove method count (#111)

* release: 0.1.0-alpha.24

* fix: lint and remove outdated method count ci test

Made-with: Cursor

---------

Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>

* feat: Adds package version resource to build (#100)

* codegen metadata

* codegen metadata

* chore: make `Properties` more resilient to `null`

* chore: drop apache dependency

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): expand imports

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

removed endpoints not in openAPI spec

* codegen metadata

* codegen metadata

* codegen metadata

* chore: align user agent format (#96)

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* chore(internal): codegen related update

* chore(internal): bump palantir-java-format

* chore(ci): skip uploading artifacts on stainless-internal branches

* chore: update placeholder string

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* fix(client): incorrect `Retry-After` parsing

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* chore(internal): tweak CI branches

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update retry delay tests

* fix(client): allow updating header/query affecting fields in `toBuilder()`

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* chore(internal): bump ktfmt

* chore: remove old test (#97)

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update gitignore

* codegen metadata

* codegen metadata

* codegen metadata

* chore(ci): skip lint on metadata-only changes

Note that we still want to run tests, as these depend on the metadata.

* Fix error messages (#102)

* codegen metadata

* Merge

* Merge

* Lint

* Add debug log

* main to next (#108)

* codegen metadata

* codegen metadata

* chore: make `Properties` more resilient to `null`

* chore: drop apache dependency

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): expand imports

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): manual updates

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

removed endpoints not in openAPI spec

* codegen metadata

* codegen metadata

* codegen metadata

* chore: align user agent format (#96)

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* chore(internal): codegen related update

* chore(internal): bump palantir-java-format

* chore(ci): skip uploading artifacts on stainless-internal branches

* chore: update placeholder string

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* fix(client): incorrect `Retry-After` parsing

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* chore(internal): tweak CI branches

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update retry delay tests

* fix(client): allow updating header/query affecting fields in `toBuilder()`

* codegen metadata

* codegen metadata

* feat(api): api update

* feat(api): api update

* codegen metadata

* chore(internal): bump ktfmt

* chore: remove old test (#97)

* codegen metadata

* feat(api): manual updates

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* chore(internal): update gitignore

* codegen metadata

* codegen metadata

* codegen metadata

* chore(ci): skip lint on metadata-only changes

Note that we still want to run tests, as these depend on the metadata.

* Fix error messages (#102)

* codegen metadata

* release: 0.1.0-alpha.24

* fix: lint and remove outdated method count ci test (#103)

Made-with: Cursor

* chore(deps): bump gradle/actions from 5 to 6 in the all-actions group (#99)

Bumps the all-actions group with 1 update: [gradle/actions](https://github.com/gradle/actions).


Updates `gradle/actions` from 5 to 6
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](gradle/actions@v5...v6)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Change to warning and add a test

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: ericdong-langchain <ericdong@langchain.dev>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Adds contributing guide (#112)

* ci: add minimum workflow permissions (#106)

- Add top-level `permissions: contents: read` to all 4 workflow files
- Change release-doctor.yml trigger from `pull_request` to `pull_request_target`
  to prevent secret exfiltration via PR-controlled workflow modifications

Co-authored-by: Posture Fix <posture-fix@langchain.ai>

* ci: SHA-pin third-party Gradle actions (#107)

Pin gradle/actions/setup-gradle and gradle/gradle-build-action to full
commit SHAs to prevent supply chain attacks via tag hijacking.

- gradle/actions/setup-gradle@v6  → @205054a...  (ci.yml ×2, codeql.yml)
- gradle/gradle-build-action@v3   → @12318b0...  (ci.yml, publish-sonatype.yml)

Co-authored-by: Posture Fix <posture-fix@langchain.ai>

* chore: fix dependabot.yml posture issues (#105)

* chore: update dependabot.yml to comply with posture checks

- Change schedule from weekly to monthly for all ecosystems
- Add update-types split (major vs minor-and-patch) for gradle and github-actions
- Add docker ecosystem entry for .devcontainer/Dockerfile

* chore: add target-branch next and fix docker group split for Stainless posture compliance

---------

Co-authored-by: Posture Fix <posture-fix@langchain.ai>

* feat(api): api update

* feat(api): api update

* feat: Add `traceable` function wrapper for LangSmith tracing (#101)

* Adds versioning resource to build

* Adds initial version of traceable

* Lint

* Deflake

* Progress

* Progress

* Fixes

* Fixes

* More refactor

* Small bug

* Refactor

* Fix

* Devin feedback

* Tests and feedback

* Remove redundant comment

* Docstring

* codegen metadata

* codegen metadata

* feat: Adds processInputs and processOutputs to traceable (#113)

* Adds processInputs and processOutputs to traceable

* Move generics into TraceProcessIO to avoid having them top level

* Fix docstring, nit

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* codegen metadata

* codegen metadata

* codegen metadata

* feat(api): api update

* codegen metadata

* feat(api): manual updates

* codegen metadata

* fix: Fix format detection when pulling legacy prompts (#115)

* Fix format detection when pulling legacy prompts

* Feedback

* Update AGENTS.md (#116)

* feat: Rework wrapOpenAI (#114)

* Update wrapOpenAI

* Remove

* Fixes

* nits

* Feedback

* Cache

* Remove comment

* Fix

* feat(api): api update

* feat(api): api update

* fix: patch security alerts — bump Jackson and constrain vulnerable transitive deps (#119)

Bumps Jackson 2.18.2→2.18.6 in published api deps and adds version
constraints for vulnerable transitive dependencies in test and example
scopes (Tomcat 9.0.115, Jetty 9.4.57, logback 1.2.13,
commons-fileupload 1.6.0, commons-io 2.14.0, json-smart 2.4.9,
snakeyaml 1.31, spring-web 5.3.34).

Addresses alerts: #75 (jackson-core), #82/#81/#80/#79/#78/#72/#70/#69
/#68/#66/#65/#60/#59/#54/#52/#51/#48/#46/#45/#44/#43/#35/#34/#29/#31
(tomcat/jetty/logback/commons).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* codegen metadata

* codegen metadata

* feat: Adds support for tracing streams with traceable (#117)

* Adds support for tracing streams with traceable

* Polish

* Make stream tracing opt-in

* Rework to use a passthrough instead of a proxy

* Record stream cancellations as errors

* Feedback

* Format and add to AGENTS.md

* feat: Adds streaming support for wrapOpenAI (#118)

* Adds support for tracing streams with traceable

* Polish

* Make stream tracing opt-in

* Rework to use a passthrough instead of a proxy

* Adds streaming support for wrapOpenAI

* Record stream cancellations as errors

* Adds streaming example

* Allow empty config default, add example

* Fix

* release: 0.1.0-alpha.25

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ericdong-langchain <ericdong@langchain.dev>
Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com>
Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com>
Co-authored-by: Posture Fix <posture-fix@langchain.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants