npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agencer/learning-loop

v0.5.0

Published

TITW Learning Loop. Exact + semantic + Composer recipe matching with Opus-driven distillation. Day 148 four-layer architecture.

Readme

@agencer/learning-loop

TITW Learning Loop. Exact + semantic + Composer recipe matching with Opus-driven distillation. The Day 148 four-layer architecture extracted from agencer-ox into a standalone, hot-swappable Lego.

The Lego short-circuits the agent pipeline when a previously-distilled recipe matches the current request. A hit returns a cached response; a miss falls through to the full pipeline and may produce a new recipe via distillation.

Status

v0.2.0. Extraction arc complete (Legs E-1 through E-5). README, Dockerfile, and Express harness make the Lego black-box reviewable in isolation. Published to npm under @agencer/learning-loop; consumable by agencer-brain, future operatives, and external integrators.

Architecture

Four matching layers, in cost-ascending order. Layer A is this Lego's public facade; the consumer dispatches on the returned HitResult.kind. Layer B is the provider implementations (pgvector for storage, Anthropic Haiku for Composer, Anthropic Opus for distillation). Layer C is the consumer composition root that selects providers and threads config.

                     ┌──────────────────────────────────────────────┐
                     │  Consumer:  lego.tryHit(rawText, ctx)        │
                     └────────────────────┬─────────────────────────┘
                                          │
        ┌─────────────────────────────────▼─────────────────────────────────┐
        │  Layer 1: Exact match  (titw_recipes.problem_signature, SHA256)   │
        │  Cost: ~1 Postgres lookup. Returns ExactHit (kind: "exact").      │
        └─────────────────────────────────┬─────────────────────────────────┘
                                          │ miss
        ┌─────────────────────────────────▼─────────────────────────────────┐
        │  Layer 2: Semantic match  (pgvector cosine, threshold default     │
        │           0.85). Returns SemanticHit (kind: "semantic").          │
        │  Cost: ~1 pgvector query.                                         │
        └─────────────────────────────────┬─────────────────────────────────┘
                                          │ miss
        ┌─────────────────────────────────▼─────────────────────────────────┐
        │  Layer 3: Composer soft-judge   (Haiku reads K near-miss          │
        │           candidates in [floor, threshold), picks one or escalates).│
        │  Cost: one Haiku call (titw.composer.attempt ledger row).         │
        │  Returns ComposerHit (kind: "composed") on "use",                 │
        │  null on "escalate" (escalate row written; no consumer delivery). │
        └─────────────────────────────────┬─────────────────────────────────┘
                                          │ escalate / no candidates
        ┌─────────────────────────────────▼─────────────────────────────────┐
        │  Escalate to full pipeline (caller-owned).                        │
        │  After pipeline completes, the consumer seals the envelope and    │
        │  calls lego.distillEnvelope(envelope) to learn a new recipe.      │
        └───────────────────────────────────────────────────────────────────┘

The four-layer split is deliberate: Layer 1 is free, Layer 2 is cheap, Layer 3 pays Haiku spend on a measurable budget, and only Layer 4 incurs full agent cost. The Lego's job is to push as many turns as safely possible into Layers 1 through 3.

See src/factory.ts:1-64 for the layer-by-layer rationale behind side effects, observability parity with the pre-extraction imperative ladder, and the F1/F2 deferral closures landed in Leg E-3e.

Quickstart

import { Pool } from "pg";
import pino from "pino";
import { UsageAccountant } from "@agencer/usage-accountant";
import { createLearningLoop, TitwService, runMigrations } from "@agencer/learning-loop";

const logger = pino();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await runMigrations(pool, logger);

const titwService = new TitwService(pool, logger);
const usageAccountant = new UsageAccountant(db, logger); // db: better-sqlite3.Database

const lego = createLearningLoop(
  { titwService, usageAccountant, logger },
  { semanticThreshold: 0.85, composerFloor: 0.55 },
);

const hit = await lego.tryHit(userText, { userId, sessionId, apiKey });
if (hit) {
  await deliver(hit.cachedResponse);
  lego.commit(hit, { userId, sessionId, apiKey });
}

Facade interface

The LearningLoop returned by createLearningLoop has four members:

| Member | Signature | Notes | |---|---|---| | tryHit | (rawText: string, ctx: TryHitContext) => Promise<HitResult \| null> | Runs the brain-gate ladder. Returns the hit shape on Layer-1 or Layer-3, null on miss / kill-switch / escalate. NEVER throws. Does NOT write hit ledger rows (deferred to commit). | | commit | (hit: HitResult, ctx: TryHitContext) => void | Call AFTER consumer-side delivery succeeds. Writes the hit ledger row, fires fire-and-forget reinforcePrinciple, emits the [BRAIN GATE] log marker. NEVER throws. Not idempotent. | | distillEnvelope | (args: DistillEnvelopeArgs) => Promise<DistillResult> | Opus-driven recipe distillation from a sealed envelope. Identity passthrough of the standalone distillEnvelope export. | | service | readonly TitwService | The underlying storage service. Exposed for flows outside the brain-gate ladder (chat-path routing, distillation-trigger background jobs). |

The delivery-gated tryHit / commit split exists so that the ledger only counts hits that the consumer actually served. If consumer-side delivery throws after tryHit returns a hit, omit commit and the ledger does not over-count.

Construction

function createLearningLoop(
  deps: LearningLoopDeps,
  config?: LearningLoopConfig,
): LearningLoop;

LearningLoopDeps:

| Field | Type | Required | Notes | |---|---|---|---| | titwService | TitwService | Yes | Recipe storage. Construct via new TitwService(pgPool, logger, options?). | | usageAccountant | UsageAccountant | Yes | Metered ledger sink. Provided by @agencer/usage-accountant. | | logger | pino.Logger | Yes | Structured logger. The facade emits [BRAIN GATE] info lines on every hit and escalate. |

LearningLoopConfig (all optional; consumer resolves env knobs and threads values):

| Field | Type | Default | Maps to env | |---|---|---|---| | semanticThreshold | number (0..1) | DEFAULT_SEARCH_THRESHOLD = 0.85 | TITW_SEMANTIC_THRESHOLD | | composerFloor | number (0..1) | DEFAULT_COMPOSER_FLOOR = 0.55 | TITW_COMPOSER_FLOOR | | composerMinConfidence | number (0..1) | DEFAULT_COMPOSER_MIN_CONFIDENCE = 0.5 | TITW_COMPOSER_MIN_CONFIDENCE | | composerTimeoutMs | number (> 0) | DEFAULT_COMPOSER_TIMEOUT_MS = 3000 | TITW_COMPOSER_TIMEOUT_MS | | composerModel | string | DEFAULT_COMPOSER_MODEL = claude-haiku-4-5-20251001 | TITW_COMPOSER_MODEL | | composerDisabled | boolean | false | TITW_COMPOSER_DISABLED | | nearMissLoggingEnabled | boolean | false | TITW_NEARMISS_LOGGING | | timeoutMs | number (> 0) | TITW_LOOKUP_TIMEOUT_MS = 2000 | (no env; per-call override only) |

Hit shapes

type HitResult = ExactHit | SemanticHit | ComposerHit;

interface ExactHit    { kind: "exact";    recipe: TitwRecipe; cachedResponse: string; }
interface SemanticHit { kind: "semantic"; recipe: TitwRecipe; cachedResponse: string; similarity?: number; }
interface ComposerHit {
  kind: "composed";
  recipeId: string;
  cachedResponse: string;
  similarity?: number;
  reasoning: string;
  confidence: number;
  adaptations?: Record<string, string>;
}

Discriminate on hit.kind. The full TitwRecipe object accompanies Layer-1 hits (exact + semantic); Composer hits carry recipeId only because the Composer's "use" decision references a candidate that was already loaded by tryComposerCandidates and does not re-fetch.

Per-call context

interface TryHitContext {
  userId: string;
  sessionId: string;
  apiKey: string;
  logFields?: Record<string, unknown>;
}

apiKey is the Anthropic API key for Composer. An empty string disables Composer (the call short-circuits to escalate). The key MUST flow per-call rather than being cached at construct time so vault rotation takes effect immediately.

logFields are spread into every [BRAIN GATE] log line. Consumers preserve their dashboard schema (for example {projectId}) without baking consumer vocabulary into the Lego. The facade's own named fields win on collision.

Integration example

A complete worked example, with all four tryHit return branches handled, lives at harness/example.ts and is compile-checked under the harness build. The condensed version:

import { Pool } from "pg";
import pino from "pino";
import { UsageAccountant } from "@agencer/usage-accountant";
import {
  createLearningLoop,
  TitwService,
  runMigrations,
  type HitResult,
} from "@agencer/learning-loop";

const logger = pino();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
await runMigrations(pool, logger);

const titwService = new TitwService(pool, logger);
const usageAccountant = new UsageAccountant(db, logger); // db: better-sqlite3.Database

const lego = createLearningLoop(
  { titwService, usageAccountant, logger },
  {
    semanticThreshold: 0.85,
    composerFloor: 0.55,
    composerDisabled: process.env.TITW_COMPOSER_DISABLED === "true",
  },
);

async function handleTurn(rawText: string, ctx: { userId: string; sessionId: string; apiKey: string }) {
  const hit: HitResult | null = await lego.tryHit(rawText, ctx);
  if (hit === null) {
    return runFullPipeline(rawText, ctx);
  }

  switch (hit.kind) {
    case "exact":
    case "semantic":
      await deliver(hit.cachedResponse);
      break;
    case "composed":
      await deliver(hit.cachedResponse);
      break;
  }

  lego.commit(hit, ctx);
}

Ledger components reference

Every metered call writes one row to UsageAccountant. The table below covers the full surface exported from the Lego barrel.

Hit and escalate (Layer-1 / Layer-3 lookup path)

| Constant | Component string | Cost | Triggered by | |---|---|---|---| | COMP_TITW_RECIPE_HIT_EXACT | titw.recipe_hit.exact | $0 | commit() after Layer-1 exact hit | | COMP_TITW_RECIPE_HIT_SEMANTIC | titw.recipe_hit.semantic | $0 | commit() after Layer-1 semantic hit | | COMP_TITW_RECIPE_HIT (deprecated alias of _EXACT) | titw.recipe_hit.exact | $0 | Back-compat for legacy external readers | | COMP_TITW_COMPOSER_ATTEMPT | titw.composer.attempt | Haiku spend | composeJudgment unconditionally on every Composer fire | | COMP_TITW_COMPOSER_HIT | titw.composer.hit | $0 | commit() after Composer decision = "use" | | COMP_TITW_COMPOSER_ESCALATE | titw.composer.escalate | $0 | tryHit on Composer decision = "escalate" (no consumer delivery) |

Distillation (Opus-driven recipe learning)

| Constant | Component string | Cost | Triggered by | |---|---|---|---| | COMP_RECIPE_DISTILLATION_OPUS | recipe.distillation.opus | Opus spend | Successful distillEnvelope call that reaches Opus | | COMP_RECIPE_DISTILLATION_SKIPPED_GATE_FAILED | recipe.distillation.skipped.gate_failed | $0 | Envelope rejected by the positive-signal gate before any Opus call | | COMP_RECIPE_DISTILLATION_SKIPPED_OPUS_ERROR | recipe.distillation.skipped.opus_error | $0 | Opus call errored (network, auth, rate-limit, server) OR TitwService.recordPrinciple threw while persisting the distilled recipe | | COMP_RECIPE_DISTILLATION_SKIPPED_VALIDATION_ERROR | recipe.distillation.skipped.validation_error | $0 | Opus response failed JSON-schema validation | | COMP_RECIPE_DISTILLATION_SKIPPED_SAFETY_CONCERN | recipe.distillation.skipped.safety_concern | $0 | Opus declared a safety concern in its structured output | | COMP_RECIPE_DISTILLATION_SKIPPED_LOW_CONFIDENCE | recipe.distillation.skipped.low_confidence | $0 | Opus confidence below the distillation floor | | COMP_RECIPE_DISTILLATION_SKIPPED_EMPTY_RESPONSE | recipe.distillation.skipped.empty_response | $0 | Opus returned a valid DistillationOutput whose positive_response field is an empty string |

Operator audit query: cost per pipeline run saved by Composer = sum(cost_usd) WHERE component = 'titw.composer.attempt' divided by count(*) WHERE component = 'titw.composer.hit'.

Env-var reference

The Lego itself never reads process.env. The consumer composition root resolves these and threads values into LearningLoopConfig (see packages/server/src/config/learning-loop-env.ts for the canonical reader pattern).

| Env var | Type | Range | Default | Maps to LearningLoopConfig | |---|---|---|---|---| | TITW_SEMANTIC_THRESHOLD | float | [0, 1] | 0.85 | semanticThreshold | | TITW_NEARMISS_LOGGING | bool | true / 1 / yes | false | nearMissLoggingEnabled | | TITW_COMPOSER_FLOOR | float | [0, 1] | 0.55 | composerFloor | | TITW_COMPOSER_TIMEOUT_MS | int | > 0 | 3000 | composerTimeoutMs | | TITW_COMPOSER_MODEL | string | non-empty | claude-haiku-4-5-20251001 | composerModel | | TITW_COMPOSER_MIN_CONFIDENCE | float | [0, 1] | 0.5 | composerMinConfidence | | TITW_COMPOSER_DISABLED | bool | true / 1 / yes | false | composerDisabled |

Invalid values (out-of-range, non-numeric, empty after trim) fall back to the documented default at consumer parse time. There is no module-load env read inside the Lego.

Standalone barrel exports

Beyond the facade, the package barrel re-exports the underlying primitives so consumers can compose flows outside the brain-gate ladder.

| Export | Purpose | |---|---| | TitwService, TitwRecipe, TitwServiceOptions, NearMiss | Storage substrate. Construct via new TitwService(pgPool, logger, options?). | | runMigrations(pool, logger) | Idempotent SQL migrations for the titw_recipes table. Run once at startup. | | normalizeProblemText(text) | Free function. Lowercase + outer-punctuation trim + collapse whitespace. Idempotent. Used internally by tryHit and distillEnvelope. | | tryRecipeHit(opts), tryComposerCandidates(opts) | The Layer-1 and Layer-3 lookup helpers. Exposed for tests and adapters that need finer granularity than tryHit. | | composeJudgment(...), buildComposerUserPrompt(...), COMPOSER_SYSTEM_PROMPT | Composer primitives. Used internally by tryHit; exported for fine-tune corpus construction. | | distillEnvelope(args), validateDistillation(json) | Distillation primitives. The facade's distillEnvelope member is an identity passthrough of the standalone function. | | LearningLoopError, LearningLoopErrorCode | Lego-owned error taxonomy (auth, network, rate_limit, server, parse, refusal, unknown). | | RecipeEnvelope, RecipeSignalKind, RecipeOpusProvider, RecipeFinalMessage | Envelope shape and provider contract for distillation. | | DistillResult, DistillSkippedReason, DistillErrorKind, DistillationCategory, DistillationOutput | Distillation result types. | | POSITIVE_SIGNAL_KINDS, DISTILLATION_SCHEMA_VERSION | Constants for the distillation gate and schema-version verbatim assertion. | | DEFAULT_SEARCH_THRESHOLD, DEFAULT_COMPOSER_FLOOR, DEFAULT_COMPOSER_MODEL, DEFAULT_COMPOSER_TIMEOUT_MS, DEFAULT_COMPOSER_MAX_TOKENS, DEFAULT_COMPOSER_TEMPERATURE, DEFAULT_COMPOSER_MIN_CONFIDENCE, TITW_LOOKUP_TIMEOUT_MS | Default values for the config fields above. | | PACKAGE_VERSION | String constant for runtime version assertions. |

How to test in isolation

# From the monorepo root:
npm run build --workspace=@agencer/learning-loop
npm run test  --workspace=@agencer/learning-loop

The Lego's test suite lives at packages/learning-loop/__tests__/:

  • factory-tryhit.test.ts: facade behavior (all four return branches, kill switch, escalate path, log-field threading).
  • composer.test.ts, composer-integration.test.ts: Composer soft-judge unit and integration coverage.
  • distiller.test.ts: Opus envelope distillation, gate, validation, skip subtypes.
  • types.test.ts: type-level assertions for the Lego-owned vocabulary.

TitwService integration tests require a Postgres instance with the pgvector extension. Set TEST_DATABASE_URL to a disposable database; the test harness runs migrations on startup and tears down test rows on teardown. The isolation guard at the repo root (commit 50670779) ensures these tests never touch a production database.

How to build the harness

# From the monorepo root:
docker build --file packages/learning-loop/Dockerfile .
docker build --file packages/learning-loop/harness/Dockerfile.harness .
docker run --rm -p 3030:3030 <harness image id>

# Smoke:
curl http://localhost:3030/health
curl -X POST http://localhost:3030/tryHit \
  -H 'content-type: application/json' \
  -d '{"rawText":"explain how to deploy"}'

The harness wraps the Lego with a four-endpoint Express server backed by a mocked TitwService. It exists for black-box review: an external dev can spin up the Lego in isolation and curl it without provisioning Postgres or pgvector. See harness/README.md for the full endpoint contract and curl recipes.

Both Dockerfiles use the monorepo root as build context (so workspace dependencies resolve at build time). After Leg E-5 published @agencer/[email protected] to npm, external consumers install from the registry instead of bind-mounting the workspace.

Sacred constraints

Per Canon Law 8, the following invariants are sacred. Changes require the explicit Learning Loop change protocol (test harness run + verification + operator sign-off):

  • Delivery-gated ledger ordering. tryHit MUST NOT write titw.recipe_hit.* or titw.composer.hit ledger rows and MUST NOT call reinforcePrinciple. Those side effects are deferred to commit, which the consumer calls AFTER successful delivery. Violations cause the ledger to count hits that were never served. The escalate ledger row IS written inside tryHit because there is no consumer-side delivery for escalate.
  • Per-call apiKey threading. The Anthropic API key for Composer flows through TryHitContext on every call. The Lego MUST NOT cache it at construct time. Vault rotation must take effect immediately on the next turn.
  • No process.env reads inside the Lego. The Lego accepts resolved config values via LearningLoopConfig. The consumer composition root owns env parsing.

License

Proprietary. See LICENSE.