* codegen metadata * codegen metadata * codegen metadata * feat(api): sandbox apis add sandbox apis * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * feat(api): api update * feat(api): api update * codegen metadata * codegen metadata * feat(api): api update * codegen metadata * codegen metadata * chore(deps): bump the minor-and-patch group across 1 directory with 5 updates (#123) * chore(deps): bump the minor-and-patch group with 5 updates Bumps the minor-and-patch group with 5 updates: | Package | From | To | | --- | --- | --- | | [org.jetbrains.dokka](https://github.com/Kotlin/dokka) | `2.0.0` | `2.2.0` | | [org.jetbrains.kotlin:kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) | `2.0.21` | `2.3.20` | | [com.vanniktech:gradle-maven-publish-plugin](https://github.com/vanniktech/gradle-maven-publish-plugin) | `0.28.0` | `0.36.0` | | [jvm](https://github.com/JetBrains/kotlin) | `2.0.21` | `2.3.20` | | [com.vanniktech.maven.publish](https://github.com/vanniktech/gradle-maven-publish-plugin) | `0.28.0` | `0.36.0` | Updates `org.jetbrains.dokka` from 2.0.0 to 2.2.0 - [Release notes](https://github.com/Kotlin/dokka/releases) - [Commits](https://github.com/Kotlin/dokka/compare/v2.0.0...v2.2.0) Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 2.0.21 to 2.3.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.0.21...v2.3.20) Updates `com.vanniktech:gradle-maven-publish-plugin` from 0.28.0 to 0.36.0 - [Release notes](https://github.com/vanniktech/gradle-maven-publish-plugin/releases) - [Changelog](https://github.com/vanniktech/gradle-maven-publish-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/vanniktech/gradle-maven-publish-plugin/compare/0.28.0...0.36.0) Updates `jvm` from 2.0.21 to 2.3.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v2.0.21...v2.3.20) Updates `com.vanniktech.maven.publish` from 0.28.0 to 0.36.0 - [Release notes](https://github.com/vanniktech/gradle-maven-publish-plugin/releases) - [Changelog](https://github.com/vanniktech/gradle-maven-publish-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/vanniktech/gradle-maven-publish-plugin/compare/0.28.0...0.36.0) --- updated-dependencies: - dependency-name: org.jetbrains.dokka dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-version: 2.3.20 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: com.vanniktech:gradle-maven-publish-plugin dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: jvm dependency-version: 2.3.20 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: com.vanniktech.maven.publish dependency-version: 0.36.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> * Fix build failures from dependency bumps Remove SonatypeHost reference removed in gradle-maven-publish-plugin 0.34.0+ (publishToMavenCentral() defaults to Central Portal). Bump proguard-gradle 7.6.0 → 7.9.0 to support Kotlin 2.3.x metadata. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert Kotlin to 2.3.0 to match Gradle 9.4.1 embedded version Kotlin 2.3.20 is not yet supported by CodeQL and conflicts with the kotlin-dsl plugin's embedded Kotlin 2.3.0. Keeping the Kotlin version aligned with the Gradle wrapper avoids both issues. Bump proguard-gradle to 7.8.0 (from 7.6.0) to support Kotlin 2.3.0 metadata. 7.9.0 has a null pointer regression; 7.8.x is the sweet spot. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.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: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * codegen metadata * codegen metadata * codegen metadata * feat(api): api update * codegen metadata * codegen metadata * feat(evaluators): add list evaluators (GET /api/v1/runs/rules) Exposes `GET /api/v1/runs/rules`. Will result in `client.evaluators.list()` in the Go SDK. The endpoint returns online evaluators (internally called "run rules") — automated rules that trigger on incoming runs to apply LLM/code evaluators, add runs to annotation queues, trigger webhooks, etc. The `runs/rules` URL path is an implementation detail; `evaluators` matches the user-facing branding. * codegen metadata * feat(api): api update * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * feat(api): api update * codegen metadata * codegen metadata * feat: support setting headers via env * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * feat: Add autobatch tracing mode (#134) * Add autobatch mode * Fix * Update signature * Add async impl * Fix optin * Fix * Fix * Fix tests * Add test * Refactor to be more idiomatic * Feedback * Use explicit lock, add to AGENTS.md * Remove test script * codegen metadata * feat: Merge enqueued posts and patches to optimize batching (#135) * Merge enqueued posts and patches to optimize batching * Exclude patched inputs by default * Logs * Fix import order * Feedback * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * feat: Make autobatch queue denote batches by payload size, add aggregation delay (#137) * Make autobatch queue denote batches by payload size, add aggregation delay * Fully drain queue when batch delay is hit * Feedback on locks * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * feat: Add multipart batching (#138) * Add multipart batching * Handle 404s better * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * codegen metadata * feat(api): api update * codegen metadata * codegen metadata * feat(api): add workspaces * feat(api): manual updates * codegen metadata * feat(api): manual updates * codegen metadata * codegen metadata * codegen metadata * feat: Add zstd compression to runs service (#136) * Add zstd compression * Merge * Fix * nits * Feedback * Fix build * codegen metadata * release: 0.1.0-beta.0 --------- 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: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Jacob Lee <jacoblee93@gmail.com>
9.6 KiB
Agent Guidelines
Code conventions and patterns for this project, learned from review feedback.
Code structure
Break up complex functions with helpers
When a function has deeply nested logic or multiple concerns, extract helpers. Use flatMap + small named functions instead of imperative loops with nested when/if:
// Good
fun format(variables: Map<String, Any>): PromptMessages {
val formatted = messages.flatMap { msg ->
if (msg.isPlaceholder()) expandPlaceholder(msg, variables)
else listOf(PromptMessage.withTemplate(msg, msg.format(variables)))
}
return PromptMessages(formatted, inputVariables, outputSchema)
}
private fun expandPlaceholder(msg: PromptMessage, variables: Map<String, Any>): List<PromptMessage> {
val items = variables[msg.template] as? List<*> ?: return emptyList()
return items.mapNotNull(::toPromptMessage)
}
// Bad — deeply nested imperative loop
fun format(variables: Map<String, Any>): PromptMessages {
val formatted = mutableListOf<PromptMessage>()
for (msg in messages) {
if (msg.isPlaceholder()) {
val value = variables[msg.template]
if (value is List<*>) {
for (item in value) {
when (item) {
is PromptMessage -> formatted.add(item)
is Map<*, *> -> { /* 15 more lines */ }
}
}
}
} else { ... }
}
}
Kotlin idioms
Prefer immutable collection transformations
Avoid mutable accumulators when map, filter, partition, associate, buildMap, or buildList express the same logic clearly. Use mutation only when it materially improves readability, performance, or is required by an API.
Use buildMap / buildList instead of mutable + convert
// Good
val messages = items.map { msg ->
buildMap<String, String> {
put("role", msg.role)
put("content", msg.content)
msg.toolCallId?.let { put("tool_call_id", it) }
}
}
// Bad — unnecessary mutable/immutable conversion
val messages = items.map { msg ->
val base = mutableMapOf<String, String>(
"role" to msg.role,
"content" to msg.content,
)
if (msg.toolCallId != null) {
base["tool_call_id"] = msg.toolCallId
}
base.toMap()
}
Use buildList for conditional toString() parts
// Good
override fun toString(): String {
val parts = buildList {
add("messages=[${messages.joinToString(", ")}]")
if (inputVariables.isNotEmpty()) add("inputVariables=$inputVariables")
commitHash?.let { add("commitHash=$it") }
outputSchema?.let { add("outputSchema=${it["title"] ?: "..."}") }
}
return "Prompt{${parts.joinToString(", ")}}"
}
// Bad — chained ternary string concatenation
override fun toString(): String =
"Prompt{messages=[...]" +
(if (commitHash != null) ", commitHash=$commitHash" else "") +
(if (hasOutputSchema()) ", outputSchema=..." else "") +
"}"
Prefer extension functions over type casts
When adding behavior to a type you don't own, use a private extension function instead of casting to an implementation type:
// Good — extension function, no cast needed
private fun Stream<*>.withErrorTracking(
errorRef: AtomicReference<Throwable>,
exhaustedRef: AtomicBoolean,
): Stream<Any?> { ... }
val instrumented = result.withErrorTracking(iterationError, streamExhausted)
// Bad — casting to implementation type
val instrumented = wrapStreamWithErrorCapture(result, iterationError, streamExhausted)
// or worse:
(runs as? RunServiceImpl)?.flush()
Use partition instead of double filter
// Good — single pass
val (systemMessages, nonSystemMessages) =
messages.partition { it.role == Role.SYSTEM }
// Bad — iterates the list twice
val systemMessages = messages.filter { it.role == Role.SYSTEM }
val nonSystemMessages = messages.filter { it.role != Role.SYSTEM }
Use parameterized tests for table-driven cases
When multiple tests share the exact same structure (input → assert same fields), use @ParameterizedTest with @MethodSource:
data class Case(val input: String, val expected: String)
@ParameterizedTest(name = "{index}: \"{0}\"")
@MethodSource("cases")
fun myTest(case: Case) {
assertThat(transform(case.input)).isEqualTo(case.expected)
}
companion object {
@JvmStatic
fun cases(): Stream<Case> = Stream.of(
Case("input1", "expected1"),
Case("input2", "expected2"),
)
}
Only do this when every test has the same assertion shape. If tests have different setup or assertions, keep them as individual @Test methods.
Extract test assertion helpers to reduce repetition
When the same assertion pattern appears across many tests, extract a helper:
// Good — readable, DRY
private fun assertMessage(msg: Map<String, String>, role: String, content: String) {
assertThat(msg["role"]).isEqualTo(role)
assertThat(msg["content"]).isEqualTo(content)
}
assertMessage(result.messages[0], "system", "You are helpful.")
assertMessage(result.messages[1], "user", "Hello")
// Bad — verbose, repetitive
assertThat(result.messages[0]).isEqualTo(mapOf("role" to "system", "content" to "You are helpful."))
assertThat(result.messages[1]).isEqualTo(mapOf("role" to "user", "content" to "Hello"))
Formatting and linting
./gradlew :langsmith-java-core:formatKotlin
./gradlew lintKotlin
The project uses ktfmt with --kotlinlang-style.
Testing
Don't add comments that restate the test name
Test function names should be descriptive enough on their own. Don't add comments that repeat what the name already says:
// Good — name is self-documenting
@Test
fun parseLegacyPromptTemplateWithTemplateFormat() {
val manifest = ...
}
// Bad — comment restates the function name
@Test
fun parseLegacyPromptTemplateWithTemplateFormat() {
// Legacy PromptTemplate format with template_format field
val manifest = ...
}
Running tests
Try to run targeted tests matching the code you changed:
# Run tests for the package you changed
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.*" --rerun
# Run a single test class
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.ManifestParserTest" --rerun
# Run a single test method
./gradlew :langsmith-java-core:test --tests "com.langchain.smith.prompts.ManifestParserTest.parsePromptTemplate" --rerun
# See println output
./gradlew :langsmith-java-core:test --tests "..." --rerun --info
Only run the full suite (./gradlew :langsmith-java-core:test --rerun) before finalizing a PR or after large cross-cutting changes.
Integration tests
Integration tests require environment variables:
export LANGSMITH_API_KEY="lsv2_pt_..."
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
Tests skip gracefully via assumeTrue if keys are missing.
Code style
- For cross-method concurrency coordination, prefer an explicit named
ReentrantLockoversynchronizedwhen review clarity matters. Use Kotlin'swithLock { ... }extension instead of manuallock()/try/finally { unlock() }unless explicit lock management is required. Keep the locked section minimal and do slow/blocking work outside the lock. - Choose the simplest concurrency primitive that fits the state being protected:
- Use atomic types (
AtomicBoolean,AtomicInteger, etc.) for simple flags, counters, and compare-and-set state. - Use
synchronizedonly for small, local critical sections where a named lock would not improve clarity. - Use
ReentrantReadWriteLockwhen reads are frequent, writes are infrequent, and concurrent reads materially help. - Use coroutine
Mutex.withLock { ... }for coroutine-based concurrency instead of blocking thread locks.
- Use atomic types (
toString()should be single-line, following theClassName{field=value, field=value}convention used by the rest of the SDK.- Avoid
@Suppress("UNCHECKED_CAST")— restructure code to use safe patterns (as? String,is Map<*, *>withentries.associate, etc). When unavoidable (e.g. generic type erasure after anischeck), add a comment explaining why the cast is safe. - Use named arguments for constructor/function calls with 2+ parameters, especially when types could be confused:
// Good PromptMessage( role = PromptMessage.Role.HUMAN, template = template, templateFormat = templateFormat, ) // Bad — positional args are ambiguous PromptMessage(PromptMessage.Role.HUMAN, template, templateFormat = templateFormat) - Name functions from the caller's perspective — describe what the caller gets, not what the function does internally. Prefer
stream.withErrorTracking()overwrapStreamWithErrorCapture(stream). - When an
Optionalhas a fallback default, useorElse(default)directly instead oforElse(null) ?: default:The// Good — default goes straight into orElse kwargs["template_format"]?.asString()?.orElse("f-string") ?: "f-string" // Bad — creates unnecessary null intermediary kwargs["template_format"]?.asString()?.orElse(null) ?: "f-string"?: "f-string"is still needed to handle the case where the key is missing from the map (nullfromkwargs["template_format"]), butorElseshould carry the default for when the key exists but isn't a string. - Anthropic SDK is a
compileOnlydependency — users must add it themselves. Methods that use Anthropic types should catchNoClassDefFoundErrorand throwIllegalStateExceptionwith a clear message.