Conversation
A tool had to exist in the LaunchDarkly AI library before an offline
evaluation could use it: every key in run(tools=...) was resolved with
GET projects/<p>/ai-tools/<key>, and that response was the only source
for the handler config's tool description and parameters.
InlineTool carries a tool's schema, description, and executable in code.
It goes in the same tools map as today's bare callables, which still mean
"library tool, resolve by key", so a map may mix the two and existing
callers are unaffected. _resolve_tools issues no request for an inline
entry; the evaluation-create body carries a source discriminator so the
record shows which identity each tool has.
Handler config synthesis is unforked -- config["tools"][key] =
{description, parameters} is fed from whichever source resolved the tool,
and an InlineTool is unwrapped to its executable before any handler sees
it, so handlers cannot tell the two apart.
Inline definitions are held to a stricter standard than server-fetched
ones, and every check runs before any network I/O: a bad inline value is
a caller bug that is cheapest to reject while no record exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tool key does not use uppercase letters. An inline key is the caller's own, so run() rejects one that carries an uppercase letter and names the key. The API key pattern is laxer and still admits one, so this rule is stricter than the server on purpose. The rule also makes the case-insensitive collision check safe. No valid pair can now differ only in case. Change the shape of the collision test. It put the inline definition on the uppercase key, which the new rule rejects first. The uppercase message also names both spellings, so the old pattern still matched and the test stopped covering the collision. The library key now carries the uppercase letter, which is the only pair that can still reach the check, and the pattern asserts the collision wording. Spec: launchdarkly/ai-sdks-monorepo#21 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This text is customer facing. Remove the event key names and the statement that the harness emits events. Say instead that the harness needs an initialized client to send results. Remove LD_API_BASE_URI and LD_UI_BASE_URI. A customer does not need either variable. State that LD_SDK_KEY is required. The old text said to configure it, which reads as optional. init_evaluations() raises without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comments in this repo are public. Remove the design rationale from the tool comments and docstrings. Each one now states what the code does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the tools mapping with a list of Tool. One Tool carries its own key. Add evals.tools.get(key, implementation=...). It reads the library tool now, pins the version now, and raises now when the tool is absent. run() reads no tool from the API. A constructed Tool is always inline. source and version are not constructor arguments, so only tools.get() can produce a library tool. Move project_key to init_evaluations(). run() no longer takes it. Read LD_PROJECT_KEY when the argument is absent. Rename the api_token argument to api_key, in init_evaluations() and in LDApiClient. The LD_API_TOKEN variable name does not change. Apply the lowercase key rule to every tool key, not only an inline one. A library key now comes from tools.get(), which shares the same check. The collision check compares keys without case. Two keys that differ only by case can no longer both be valid, so the earlier asymmetry is gone. The create body and the event payloads do not change. Spec: launchdarkly/ai-sdks-monorepo#21 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Lets a developer define a tool in code instead of creating it in LaunchDarkly first.
_resolve_tools(evaluations/runner.py) currently requires every key in thetoolsmap to resolve viaGET projects/<p>/ai-tools/<key>, and that response is the only source for the handler config's tooldescriptionandparameters. There is no slot in the public API for a caller-supplied body.Changes
InlineTooltype carryingdescription,schema, and the executable (implementation), accepted in the sametoolsmap as today's bare callables. A bare callable still means "library tool, resolve by key" — no change for existing callers, and a map may mix both sources._resolve_toolsskips theai-toolsGET for inline entries._validate_tools, called fromrun()ahead of every request): key non-blank,schemaa JSON object and JSON-serializable withallow_nan=False, implementation callable, no duplicate keys across sources, and an inline key that also names a library tool is rejected naming the key.{key, schema, description, source: "inline"}for inline entries and{key, version, source: "library"}for library entries.toolsis still omitted entirely when the map is empty.config["tools"][key] = {description, parameters}is fed from the inline body instead of the GET response, and anInlineToolis unwrapped to its executable before any handler sees it, so handlers see no difference.Decision for reviewers
Whether a
NativeToolvalue may be paired with an inline definition. A native tool has no schema of its own, so I've made the combination a validation error rather than inventing a meaning for it.One other thing worth a look: the brief asked for "no duplicate keys across sources" and "an inline key that also names a library tool is rejected". Because both sources share one
toolsmapping, a plaindictcannot express the same key twice, so those checks needed a concrete surface. Implemented as: (a) the map is validated as the caller'sMappingbefore it is copied into adict, so aMappingthat yields a key twice is caught rather than silently collapsed; and (b) keys are compared case-insensitively only when an inline definition is involved —{"lookup_order": fn, "Lookup_Order": InlineTool(...)}is rejected naming the key, while two library keys differing only by case remain two library lookups, since changing that would alter existing library behavior. Both are tested. Say the word if you'd rather the collision rule were exact-case only, or extended to library/library pairs.Verification
Run in a clean worktree off
origin/main:uv run pytest— 1318 passed, 11 skipped (full monorepo suite).uv run ruff check .— all checks passed.uv run ruff format --check .— 114 files already formatted.uv run mypy packages/*/src(the repo'smake typecheck, which CI mirrors) — no issues in 50 source files. Note the repo does not type-check test suites understrict, perAGENTS.md, so the new tests are not mypy-covered.New tests in
packages/client/tests/test_evaluations_run.py:test_inline_tool_runs_without_reading_the_tool_api— asserts the inline request sequence in full as(method, path)pairs, and asserts explicitly that no recorded URL contains/ai-tools, rather than relying on the strict-sequence transport to trip on a surplus request. Also asserts the create body's inline entry, the synthesizedconfig["tools"], and that the handler receives the bare callable.test_inline_tool_description_defaults_to_empty_stringtest_mixed_library_and_inline_tools_each_keep_their_own_source— exactly one tool GET, for the library key only; both wire entries; both config entries.test_bad_tool_entry_is_rejected_with_zero_requests— parametrized over blank inline key, blank library key,schema=None,schemaas a list, non-serializableschema,NaN,Infinity, non-callable implementation, non-string description, and an entry that is none of the three. Each assertstransport.requests == [].test_native_tool_paired_with_an_inline_definition_is_rejected— zero requests.test_native_tool_on_its_own_still_resolves_from_the_library— the library path forNativeToolis unchanged.test_inline_key_that_also_names_a_library_tool_is_rejected— zero requests.test_two_library_keys_differing_only_by_case_are_still_two_lookups— pins the library path against the new collision rule.test_duplicate_tool_key_across_sources_is_rejected_with_zero_requests— uses a customMappingthat yields one key twice.One existing assertion changed: the happy-path create body in
test_complete_run_with_zero_failed_and_error_rows_passesnow expects{"key": "lookup_order", "version": 7, "source": "library"}.CI on this branch is green: Lint & format, Type check, Tests, Build all packages, Install & sync all pass.
Not verified: nothing was exercised against a real gonfalon proxy or ai-evaluator — the
sourcediscriminator and the inline body shape are asserted against the recording fake transport only.Dependencies
Requires the gonfalon proxy/API change deployed. Cross-language spec is ai-sdks-monorepo
TESTING.md§8 (spec PR open concurrently).🤖 Generated with Claude Code