Skip to content

SMOODEV-2306: Add limits — the fourth @smooai/config kind#114

Merged
brentrager merged 1 commit into
mainfrom
SMOODEV-2306-limits-config-kind
Jul 4, 2026
Merged

SMOODEV-2306: Add limits — the fourth @smooai/config kind#114
brentrager merged 1 commit into
mainfrom
SMOODEV-2306-limits-config-kind

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Problem

@smooai/config has three kinds: publicConfigSchema, secretConfigSchema, featureFlagSchema. None fit a soft, tunable numeric target — a value that resolves differently per { orgId, agentId, plan, … }, flips without a redeploy, and carries its own clamp so a stale/bad server value can't push a consumer out of a safe range. Concretely, smooth-operator needs agentMaxIterations per {orgId, agentId} to replace the SMOOTH_AGENT_MAX_ITERATIONS env stopgap.

Solution — a numeric sibling of feature flags

Limits are the fourth kind: numeric, segment-resolved, clamp-aware, resolved live (never baked) through the SAME server segment evaluator as feature flags. A limit is a soft target — consuming code still hard-clamps; the config tunes within a client-applied [min, max]. No new resolution engine: the client code paths parallel the feature-flag evaluator exactly.

  • Schema (TS): defineConfig({ limitsSchema }) + defineLimit({ default, min, max, step }), exposing LimitKeys + _limitsMeta, serialized as bounded JSON-Schema number nodes in the smooai-config push wire format. Limits stay out of the bake/env/file resolution chain (they never bake).
  • Clamp (clampLimit, pure + identical in every language): non-finite/non-numeric → default; snap to step; clamp into [min, max]. Lives on the client so a stale server value can't escape the declared range.

Per-language surface

lang evaluate clamp getLimit
TS client.evaluateLimit(key, ctx?, env?)EvaluateLimitResponse clampLimit(raw, def) config.limit.getLimit(key) / getClientLimit
Rust client.evaluate_limit(key, ctx, env).await clamp_limit(raw, &spec)
Python client.evaluate_limit(key, ctx?, env?) clamp_limit(raw, spec)
Go client.EvaluateLimit(ctx, key, evalCtx, env) ClampLimit(raw, spec)
.NET client.EvaluateLimitAsync(key, ctx?, env?, ct) spec.Clamp(raw)

Each with typed Limit* errors mirroring the feature-flag hierarchy (404/400/5xx). Also createLimitEvaluator factory + a limit tier (getLimit sync + evaluateLimit async, clamped) on buildClientConfig / buildConfig.

Endpoint contract: POST /organizations/{org_id}/config/limits/{key}/evaluate.

Test notes

Tests pass in all five languages:

  • TS (vitest): full suite 288 passed; new schema/clamp + client evaluate + tier tests
  • Rust (cargo): 289 passed; clippy -D warnings clean
  • Python (pytest): 38 in test_client; ruff + basedpyright clean
  • Go (go test): 353 passed; go vet clean
  • .NET (xunit): 143 passed

pnpm typecheck + oxfmt --check + oxlint (0 errors) clean.

Known gaps (see DESIGN-limits.md)

  1. Schema declaration is TS-only — limits are authored in TS .smooai-config and pushed; polyglot runtimes consume (evaluate) them, so native define_config signatures were left at three tiers.
  2. Server endpoint/config/limits/{key}/evaluate is documented + called here; wiring the route (alias to the numeric segment evaluator) is the smooai-monorepo follow-up.
  3. Bundler baking of limit defaults + React useLimitEvaluation deferred (the latter waits on useFeatureFlagEvaluation, which never landed).

Docs: DESIGN-limits.md (full design + rollout), SCHEMA_USAGE.md (usage), changeset added. Links SMOODEV-2306.

🤖 Generated with Claude Code

Limits are numeric, segment-resolved, clamp-aware config values. They flow
through the SAME server segment evaluator as feature flags (resolved live,
never baked) and tune a soft target within a client-applied [min, max] clamp,
replacing env stopgaps like SMOOTH_AGENT_MAX_ITERATIONS. Motivating consumer:
smooth-operator reads `agentMaxIterations` by {orgId, agentId}.

Why a new kind: none of public/secret/featureFlag fit a soft numeric target
that resolves per-segment and carries its own clamp metadata so a stale/bad
server value can never push a consumer out of a safe range.

- Schema (TS): defineConfig({ limitsSchema }) + defineLimit({default,min,max,step}),
  exposing LimitKeys + _limitsMeta, serialized as bounded JSON-Schema number
  nodes in the push wire format. Limits stay OUT of the bake/resolution chain.
- TS client: ConfigClient.evaluateLimit, createLimitEvaluator, getClientLimit,
  clampLimit, and a `limit` tier (getLimit sync + evaluateLimit async, clamped)
  on buildClientConfig / buildConfig. Typed Limit* error classes.
- Polyglot parity mirroring the feature-flag evaluator: Rust evaluate_limit +
  clamp_limit, Python evaluate_limit + clamp_limit, Go EvaluateLimit + ClampLimit,
  .NET EvaluateLimitAsync + LimitSpec.Clamp — each with typed Limit* errors.
- Endpoint contract: POST /organizations/{org_id}/config/limits/{key}/evaluate.
- Tests pass in all five languages (TS/Rust/Python/Go/.NET). Design + rollout
  gaps in DESIGN-limits.md; usage in SCHEMA_USAGE.md; changeset added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 99d918a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/config Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brentrager brentrager merged commit 6377b67 into main Jul 4, 2026
1 check passed
@brentrager brentrager deleted the SMOODEV-2306-limits-config-kind branch July 4, 2026 21:52
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.

1 participant