Skip to content

feat!: Add per-execution runId, at-most-once tracking, and cross-process tracker resumption#29

Merged
jsonbailey merged 23 commits into
mainfrom
jb/aic-2207/update-ai-sdks-billing-spec
May 14, 2026
Merged

feat!: Add per-execution runId, at-most-once tracking, and cross-process tracker resumption#29
jsonbailey merged 23 commits into
mainfrom
jb/aic-2207/update-ai-sdks-billing-spec

Conversation

@jsonbailey
Copy link
Copy Markdown
Contributor

@jsonbailey jsonbailey commented Apr 15, 2026

BEGIN_COMMIT_OVERRIDE
feat!: Add per-execution runId, at-most-once tracking, and cross-process tracker resumption
feat!: Replace tracker tuple from completion_config with AIConfig#create_tracker factory
feat!: Track each AIConfigTracker metric at most once per tracker
feat: Add per-execution runId to correlate AIConfigTracker events
feat: Add Client#create_tracker(token:, context:) to resume a tracker across processes
END_COMMIT_OVERRIDE

Summary

  • Per-execution runId: Every tracker event now includes a unique runId (UUID v4) for billing isolation and deduplication
  • At-most-once semantics: Each metric type (duration, tokens, success/error, feedback, TTFT) can only be tracked once per tracker instance; subsequent calls are dropped with a warning
  • AIConfig#create_tracker factory: The completion config object returned by Client#completion_config exposes a create_tracker method that returns a fresh AIConfigTracker with a new runId on each call, replacing the old single-tracker pattern. The SDK always supplies a working tracker factory, even for disabled flag evaluations.
  • AIConfigDefault / AIConfig are independent classes: AIConfigDefault is the application-supplied fallback type (passed as default: to Client#completion_config); AIConfig is the SDK-returned type that carries a tracker factory. Direct construction of AIConfig is not supported.
  • Resumption token: AIConfigTracker#resumption_token returns a URL-safe Base64-encoded JSON token ({ runId, configKey, variationKey, version }) for cross-process tracker reconstruction
  • Client#create_tracker(token:, context:): Reconstructs a tracker from a resumption token for deferred tracking (e.g. feedback from a different process). modelName and providerName are set to empty strings on reconstruction.
  • base64 gem dependency: Added as a runtime dependency (removed from Ruby 3.4 default gems)

Test plan

  • AIConfig#create_tracker returns new tracker instances with distinct runIds
  • AIConfig#create_tracker preserves flag metadata (configKey, variationKey, version, modelName, providerName)
  • AIConfigDefault.disabled returns a disabled fallback config
  • AIConfig is not a subclass of AIConfigDefault and does not expose a disabled class method
  • AIConfig.new requires a tracker_factory: keyword argument
  • Trackers from the same config have independent at-most-once tracking state
  • resumption_token encodes exactly { runId, configKey, variationKey, version } (no modelName/providerName)
  • from_resumption_token round-trips correctly and sets modelName/providerName to ""
  • Restored tracker can send track events with the original runId
  • Client#create_tracker round-trips through resumption token
  • All 57 tests pass (0 failures)

🤖 Generated with Claude Code


Note

High Risk
High risk because it changes the public tracking API (AIConfig/default handling) and modifies emitted analytics event payloads by introducing per-run runId and at-most-once semantics, which could affect downstream metrics/billing expectations.

Overview
Introduces per-execution tracking by minting a UUIDv4 runId for each AI run and attaching it to all tracker events, with a new AIConfig#create_tracker factory replacing the previous single-tracker pattern.

Adds at-most-once semantics to AIConfigTracker metric methods (duration, TTFT, tokens, success/error, feedback), dropping subsequent calls with warnings, and implements cross-process tracker resumption via AIConfigTracker#resumption_token plus Client#create_tracker(token:, context:).

Splits fallback vs returned config types by adding AIConfigDefault for completion_config(default:), removes AIConfig.disabled, updates examples/tests accordingly, and adds a runtime base64 dependency.

Reviewed by Cursor Bugbot for commit 86bcf1f. Bugbot is set up for automated code reviews on this repo. Configure here.

- Each tracker now carries a run_id (UUIDv4) included in all emitted
  events, scoping every metric to a single execution
- At-most-once semantics: duplicate calls to track_duration,
  track_tokens, track_success/track_error, track_feedback, and
  track_time_to_first_token on the same tracker are dropped with a
  warning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jsonbailey jsonbailey changed the title feat!: Add per-execution runId and at-most-once event tracking feat!: Add per-execution runId, at-most-once tracking, and cross-process tracker resumption Apr 15, 2026
…ess tracker resumption

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jsonbailey jsonbailey force-pushed the jb/aic-2207/update-ai-sdks-billing-spec branch from fc11d5d to e2d74cf Compare April 16, 2026 16:53
jsonbailey and others added 5 commits April 16, 2026 13:39
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n token

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jsonbailey jsonbailey marked this pull request as ready for review April 16, 2026 22:05
@jsonbailey jsonbailey requested a review from a team as a code owner April 16, 2026 22:05
Comment thread lib/server/ai/ai_config_tracker.rb Outdated
Comment thread lib/server/ai/client.rb
jsonbailey and others added 2 commits April 16, 2026 17:20
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per AICONF spec 1.2.7.1, create_tracker always returns a new tracker
instance. The evaluation path always sets a factory, so the safe
navigator is unnecessary. Updated test to verify disabled configs
from evaluation still produce a tracker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread lib/server/ai/client.rb
jsonbailey and others added 6 commits April 17, 2026 15:33
Each ask_agent invocation now creates its own tracker via create_tracker,
returns a resumption token alongside the response, and deferred feedback
is correlated back to the specific invocation by reconstructing the tracker
from the token at the call site.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Disabled configs are an internal client concern. AIConfig.disabled had
no tracker factory, which could cause errors if create_tracker was called
on it. The client now uses a private DISABLED_AI_CONFIG_DEFAULT hash
constant as its internal fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split AIConfig into two types to match the js-core pattern:
- AIConfigDefault: user-facing fallback type with enabled, model,
  messages, provider (no tracker factory). Used as default: parameter.
- AIConfig: SDK-returned type with tracker_factory as a required kwarg,
  ensuring create_tracker is always available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t.disabled

The constant was only referenced once — inline AIConfigDefault.disabled
in completion_config and remove the private hash constant entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes Style/KeywordParametersOrder rubocop offense by placing the
required keyword parameter first in AIConfig#initialize.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread lib/server/ai/client.rb
jsonbailey and others added 4 commits April 21, 2026 18:04
AIConfigDefault serves as the base class holding the shared attr_readers
(enabled, messages, model, provider) and to_h. AIConfig extends it with
tracker_factory and create_tracker only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reword the six at-most-once warning messages to lead with the method
name ("Skipping <method>:") and tell the user how to recover ("Call
create_tracker on the AI Config for a new run"). Matches the wording
applied to the Go SDK in PR #363.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand the AIConfigTracker class doc and AIConfig#create_tracker doc to
explain runId, how all events emitted by a tracker share a runId so they
correlate in metrics views, and that a resumption token preserves the
runId across processes.

Add per-method "Records at most once per Tracker" paragraphs to
track_duration, track_time_to_first_token, track_feedback, and
track_tokens. Note shared-state behavior on track_success and
track_error. Note that track_openai_metrics and
track_bedrock_converse_metrics will re-run the inner block but emit no
additional metric events on repeat calls.

Sweep remaining "execution" wording in doc-comments and replace with
"AI run". Public method names and on-the-wire event names are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The reworded warning exceeded the project's 180-character line limit.
Split it into two adjacent string literals; the resulting string is
unchanged at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread lib/server/ai/client.rb
jsonbailey and others added 3 commits May 13, 2026 11:57
Rephrase the two doc sentences that placed "run" and "runId" next to
each other. The AIConfigTracker class doc now says a reconstructed
tracker "shares the original runId" instead of "correlates with the
original run". The AIConfig#create_tracker doc now describes the runId
as correlating "the tracker's events" rather than "the run's events".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A tracker built from _completion_config defaults variation_key to '' when
_ldMeta.variationKey is absent. resumption_token correctly omits empty
strings from the encoded payload, but from_resumption_token previously
defaulted the missing key to nil, so the reconstructed tracker exposed
variation_key as nil instead of ''.

The wire payload was unaffected because flag_data guards on both nil and
empty, but the asymmetry was visible via the public attr_reader and
brittle: any future simplification of either guard would diverge the
two paths.

Default the decode to '' to match how fresh trackers are constructed in
client.rb, and to mirror the existing empty-string defaults for
model_name and provider_name in the same method. Add a spec covering
the empty-variation_key round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AIConfig and AIConfigDefault are now parallel hierarchies rather than
parent/child. The two types serve different roles -- AIConfigDefault is
an application-supplied fallback value passed into the SDK, AIConfig is
what the SDK returns and always carries a working tracker factory --
and inheritance was producing footguns at the boundary:

  * AIConfig inherited AIConfigDefault.disabled, which dispatched to
    AIConfig.new(enabled: false) and raised ArgumentError because
    tracker_factory is required.
  * AIConfigDefault.disabled.create_tracker raised NoMethodError because
    AIConfigDefault has no create_tracker.

Both crashes are now impossible by construction: AIConfig has no
disabled class method, and AIConfigDefault is never expected to mint
trackers. The shared fields (enabled, model, messages, provider) and
to_h are intentionally duplicated -- the type separation is the point.

Update the commented fallback examples in hello_bedrock.rb and
hello_openai.rb to use AIConfigDefault.new(...) (the public way to
build a fallback). Add three specs locking in the new invariants:
AIConfig does not respond to .disabled, AIConfig.new requires
tracker_factory, and AIConfig is not in AIConfigDefault.ancestors.

The inheritance was only introduced earlier on this branch (commit
8d1a66b) and never shipped, so no released API is broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default mode and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 822734b. Configure here.

Comment thread lib/server/ai/ai_config_tracker.rb
The previous wording on track_openai_metrics and
track_bedrock_converse_metrics claimed a second call produces "no
additional metric events". That is wrong for partial-failure cases: if
the first call recorded duration and success but the inner block had no
usage data, a second call where the block returns usage can still emit
token metrics, because track_tokens has not yet recorded on this
Tracker.

Rephrase both doc paragraphs to describe the actual behavior -- only
metrics not already recorded are emitted -- and point users at
create_tracker for a clean run, matching the call-to-action in the
at-most-once warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jsonbailey jsonbailey merged commit 20f06f1 into main May 14, 2026
10 checks passed
@jsonbailey jsonbailey deleted the jb/aic-2207/update-ai-sdks-billing-spec branch May 14, 2026 16:58
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