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

@geovanelourenco/conductr-core

v0.4.1

Published

The execution and knowledge engine for AI over your own data: scoped reads through typed capabilities, answers from evidence with any model, and memory with review, versions and search.

Downloads

472

Readme

@geovanelourenco/conductr-core

The execution and knowledge engine for AI over your own data.

  • Execution: answers questions by reading your data through typed capabilities, with the caller's scope enforced by your code, every model reply validated, and every fact traced back to the records it came from. Works with any model, small and local or large and hosted.
  • Knowledge: remembers what matters between conversations (facts, preferences, goals, procedures) with review, versions, scopes, provenance and full-text search.

Install

npm install @geovanelourenco/conductr-core

Requirements:

  • Node.js 20.3 or later.
  • An OpenAI-compatible server: one that honours response_format.schema for grounded (llama.cpp's llama-server does), or one with tool calling for agent.
  • For the SQLite knowledge store: Node.js 22.5 or later (node:sqlite), or better-sqlite3.

Tested with Qwen3 1.7B (Q4_K_M) on CPU.

llama-server --hf-repo unsloth/Qwen3-1.7B-GGUF:Q4_K_M --alias local --ctx-size 4096 --port 8080

Execution

The engine serves every provider. What a provider can do decides how a question is answered:

  • grounded, for any provider and the safe choice for small models: the model only makes small, constrained choices and your code does everything else (below).
  • agent, for providers that call tools natively (Gemini, OpenAI, a large local model with a tool-calling template): the model calls your capabilities as tools, as many times as it needs, and writes the answer. The scope, the validation and the limits stay with the runtime.

With strategy: "auto" (the default) the engine picks agent when the model client has chat, and grounded otherwise.

Grounded

  1. Plan. The model labels the question: a read the capabilities can answer, or one of your refusal labels (writes, secrets, another tenant…). For a read it also lists the periods the question names, so a comparison of two months becomes two reads.
  2. Read. For each period the model fills a capability input whose JSON Schema you provide: your enums, your ids for this user only, and the period already fixed. Your capability runs the read with the caller's trusted scope.
  3. Answer. Facts are taken from the rows your capability returned, each with the records it came from, plus the changes between periods. Your formatAnswer writes the answer from them. No number in the answer comes from model text.

Every reply is constrained by a JSON Schema, and the model server compiles it into a grammar. A reply that still fails validation goes back to the model with the exact problem.

Usage

import { CapabilityInputError, createConductr, memoryCache, openAiCompatibleModel } from "@geovanelourenco/conductr-core";

const expenses = {
  id: "expenses.sum",
  version: "v1",
  summary: "totals of the family's expenses, optionally per category",
  describe: (scope) => `Categories: ${categoriesOf(scope).map((c) => `${c.id} = ${c.name}`).join(", ")}.`,
  inputSchema: (scope, hints) => ({
    type: "object",
    properties: {
      category: { enum: ["any", ...categoriesOf(scope).map((c) => c.id)] },
      from: hints.period ? { const: hints.period.from } : { type: "string" },
      to: hints.period ? { const: hints.period.to } : { type: "string" },
    },
    required: ["category", "from", "to"],
    additionalProperties: false,
  }),
  async execute(input, { scope }) {
    if (!isValid(input)) throw new CapabilityInputError("from and to must be YYYY-MM-DD."); // shown to the model
    const rows = await db.sumExpenses({ ...input, familyId: scope.familyId }); // scope from your session
    return { rows: [{ total_cents: rows.total, count: rows.count }], recordIds: rows.ids };
  },
};

const conductr = createConductr({
  models: { planner: openAiCompatibleModel({ url: "http://127.0.0.1:8080" }) },
  capabilities: [expenses],
  refusals: [
    { label: "changes_data", description: "create, change or delete records" },
    { label: "moves_money", description: "transfers or payments" },
  ],
  formatAnswer: ({ facts }) => facts.map((fact) => `${fact.name}: ${fact.value}`).join("\n"),
  cache: memoryCache(),
});

// Stream progress to your client (over SSE, for instance)…
for await (const event of conductr.ask({ question, scope: session.scope, requestId })) {
  send(event);
}

// …or just wait for the result.
const result = await conductr.run({ question, scope: session.scope, requestId });

Capabilities

interface Capability<TScope> {
  id: string;
  version: string;
  summary: string;                                    // for the planner
  describe(scope: TScope): string;                    // how to fill the input, with reference data the scope may see
  inputSchema(scope: TScope, hints: { period?: Period }): JsonSchema;
  execute(input: unknown, context: { scope: TScope; signal?: AbortSignal }): Promise<{ rows; recordIds }>;
}

Guidelines that matter with small models:

  • Prefer closed choices. Use enums of the caller's own ids, and list "any" first. A small model picks reliably from a list and invents ids when it has to write them.
  • Match the input to the provider. For small models, a few question-shaped fields that you compile into your query are far more reliable than a free query language. Large models with tool calling handle an open query (filters, grouping, measures) well, and it covers far more questions. The same capability can serve both: the runtime does not care, the model does.
  • Shape the rows. Rows without an id are aggregates: their numbers become facts, labelled by their string values. Rows with an id are records. Leave scope columns out of the rows.
  • Validate everything in execute. Throw CapabilityInputError for inputs the model can fix.

Agent strategy

const model = openAiCompatibleModel({
  url: "https://generativelanguage.googleapis.com/v1beta/openai",
  model: "gemini-3.5-flash-lite",
  apiKey: process.env.GEMINI_API_KEY,
  tools: true,             // the provider calls tools: the client gets `chat`
  disableThinking: false,  // llama.cpp's chat_template_kwargs is for local models
  reasoningEffort: "low",
});

const conductr = createConductr({
  models: { planner: model },
  capabilities: [query],
  instructions: "Answer in Brazilian Portuguese, in up to four sentences.",
  tools: [rememberTool(engine, { scopes })], // optional: anything besides reads
  maxRounds: 5,       // model turns; the last one must answer
  maxToolCalls: 8,
});

for await (const event of conductr.ask({ question, scope, history })) send(event); // `text` events stream the answer
  • Each capability is offered as a tool (summary and describe(scope) as its description, inputSchema(scope, {}) as its parameters). Invalid JSON, unknown tools and CapabilityInputErrors go back to the model to fix.
  • The refusal labels become a refuse tool; calling it ends the request as refused.
  • history carries earlier turns, for follow-ups, and instructions on ask adds rules for that question only (the person's preferred tone, for instance) after the shared ones.
  • A tool with requiresApproval: true never changes anything: its execute validates the call and returns proposal items (id, a one-line summary, and your own data). The model is told the changes wait for approval. All the items of an answer, from every call, come together in one proposal event before done (and in result.proposal), so the person approves them at once, or some of them; applying them is yours, after approval.
  • A capability may return content, what the model reads instead of { rows } (a compact, already formatted view), and label, a short description of the read that comes back in part_finished for the host to show ("expenses in October").
  • The answer is the model's text. facts still come from the rows each read returned, so a host can check the numbers against them.

Knowledge

@geovanelourenco/conductr-core/knowledge keeps what an assistant learns: facts, preferences, goals, procedures and decisions. It is a module of its own, built as use cases over a storage contract; only knowledge/conductr.ts knows the runtime, so it can move to a separate package.

import { DatabaseSync } from "node:sqlite"; // or better-sqlite3
import { createKnowledgeEngine, knowledgeContext, rememberTool, sqliteKnowledgeStore } from "@geovanelourenco/conductr-core/knowledge";

const engine = createKnowledgeEngine({
  store: sqliteKnowledgeStore(db, { prefix: "assistant_memory" }),
  // Default: every proposal waits for review. Here a person's own preferences are kept at once (show them with an undo).
  autoAccept: (candidate) => candidate.scope.type === "user" && candidate.kind === "preference",
});

const scopes = (session) => ({ person: { type: "user", id: session.userId }, family: { type: "family", id: session.familyId } });
const conductr = createConductr({
  // ...
  context: knowledgeContext(engine, { scopes: (session) => Object.values(scopes(session)) }),
  tools: [rememberTool(engine, { scopes })],
});
  • Review. propose records a candidate; accept turns it into knowledge, reject keeps the reason. The autoAccept policy decides what skips review.
  • Scopes. Every read takes the scopes the caller may see, from your session. The remember tool offers the model scope labels (person, family), never ids.
  • Versions. A proposal with supersedes becomes the next version of that entry; history lists them all. forget stops recalling an entry and keeps the record.
  • Provenance and validity. Each entry keeps where it came from, and validUntil retires a goal after its deadline. Recalled knowledge is labelled as possibly outdated: data read now wins.
  • Decisions. recordDecision keeps how a person decided on a proposal (approved, rejected or in part, with the reason), at once and whatever the review policy, so later answers recall it through knowledgeContext and stop proposing what was turned down.
  • Search. SQLite FTS5, ignoring accents and matching prefixes, ranked by bm25. Without FTS5 the store scans the text. The index is a projection: rebuildIndex recreates it from the canonical tables.
  • Storage. sqliteKnowledgeStore takes any connection with exec and prepare (Node's node:sqlite, better-sqlite3) and creates its tables under a prefix. Implement KnowledgeStore for another database.

Learning

Knowledge about the people the assistant serves makes it fit each of them; learning makes the assistant itself better at its job. createLearner turns what happened in an answer, and what came back about it, into lessons for the agent: rules of kind procedure, in a scope the host keeps for the agent and shares with everyone it serves.

import { combineContext, createLearner, episodeRecorder, knowledgeContext } from "@geovanelourenco/conductr-core/knowledge";

const learner = createLearner({
  model,                                    // the model that reflects (any client)
  engine,
  scope: { type: "agent", id: "finance" },  // the agent's own lessons, never a person's scope
  instructions: "A family finance assistant. consultar reads entries; propor_alteracoes prepares changes.",
  confirmations: 3,                         // optional: accepted on its own once 3 different origins confirm it
});

const conductr = createConductr({
  // ...
  context: combineContext(knowledgeContext(engine, { scopes }), learner.context()),
});

// While answering: keep what the agent did, with the message.
const recorder = episodeRecorder(question);
for await (const event of conductr.ask({ question, scope })) { recorder.observe(event); send(event); }
save(messageId, recorder.episode());

// When feedback arrives (minutes or days later):
await learner.learn({
  episode: load(messageId),
  signal: { type: "rating", rating: "down", comment: "I wanted to recategorize them, not create an entry" },
  origin: hash(tenantId),
});
  • Episode. episodeRecorder builds, from the events of one ask, a compact JSON of the request, each read or tool call with its input (and whether it was rejected), the changes prepared and the answer. The host keeps it until feedback comes.
  • Signals. A rating (up or down, with an optional comment), a decision on a proposal (approved, rejected, partial, with the reason) or a correction: the person's next message, when it looks like it says the answer missed. It need not be sure: a message that turns out to be a follow-up or a new request teaches nothing.
  • Reflection. One model call compares what was asked, what was done and what came back, with the related lessons already known, and decides: a new lesson, the same as a known one, a correction of a known one (its next version, through supersedes), or none: taste, tone, a problem with the data, vague feedback or a right answer teach nothing. Lessons are written as general rules, without names, amounts, dates or anything that identifies who asked. A reply it cannot use goes back once with the problem.
  • Review. Lessons are proposals: they wait for a person (learner.pending, learner.accept, learner.reject) unless the engine's autoAccept takes them. With confirmations, a lesson confirmed by that many different origins is accepted on its own; confirmations are kept as proposals pointing at the lesson and settled when it is decided.
  • Recall. learner.context() adds the accepted lessons that match the question to the prompt; combineContext puts them after what the assistant knows about the person.

Results and events

run resolves, and ask ends with { type: "done", result }. The result is one of three shapes:

  • completed: strategy, answer, facts (each with recordIds and source) and parts. parts holds what was read and with which input; you can show that interpretation to the user and let them correct it.
  • refused: label and reason.
  • failed: error.code, one of model_unavailable, invalid_model_output, capability_failed or aborted.

Every result also carries usage (model calls, corrections, tokens) and durationMs.

Events, in order: strategy; in grounded, planned, then for each part part_started, query (and query_rejected when the capability refuses an input), and part_finished; in agent, the same part events for each read, text as the answer is written and tool for other tools; then done.

Retrying

Everything is read-only, so resending a failed question is safe. With a cache and a requestId, resending a question that already finished returns the stored result without calling the model. Failed results are not cached.

Options

| Option | Default | | |---|---|---| | models.planner / models.worker | — / planner | a larger model can plan while a smaller one reads | | refusals | DEFAULT_REFUSALS | labels the planner may refuse with | | periods | true | split questions into the periods they name | | formatAnswer | one line per fact | write the answer in your language and number format | | corrections | 2 | invalid replies sent back to the model | | queryRetries | 2 | inputs a capability may reject before a part fails | | cache | none | memoryCache({ ttlMs }), or your own { get, set } | | now | new Date() | the reference date for months without a year | | strategy | auto | grounded, agent, or by provider | | context | none | extra context per question, such as knowledgeContext | | instructions | none | agent: the host's rules, added to the system prompt | | tools | none | agent: tools besides the capabilities | | maxRounds / maxToolCalls | 5 / 8 | agent: model turns and calls per question | | maxResultChars | 8000 | agent: longest tool result sent to the model |

openAiCompatibleModel({ url, model, timeoutMs = 60000, temperature = 0.2, disableThinking = true, apiKey, tools = false, structuredOutput = "llama", reasoningEffort }) builds a model client. tools: true adds chat; structuredOutput: "json_schema" asks hosted providers for a schema the OpenAI way.

When the model answers HTTP 429 or 5xx, the client tries it again (retries, default 1, retryDelayMs apart) and then each of fallbackModels in order. An overloaded model is skipped for cooldownMs (default 5 minutes), so later requests go straight to the fallbacks. onRetry reports each switch, for your logs. Implement ModelClient to use any other server.