@bitbaum/ai-kit 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/attempt.d.ts +48 -0
  4. package/dist/attempt.js +59 -0
  5. package/dist/catalog.d.ts +65 -0
  6. package/dist/catalog.js +115 -0
  7. package/dist/chain.d.ts +204 -0
  8. package/dist/chain.js +261 -0
  9. package/dist/fair-share.d.ts +120 -0
  10. package/dist/fair-share.js +127 -0
  11. package/dist/forms.d.ts +15 -0
  12. package/dist/forms.js +15 -0
  13. package/dist/grounding/contract.d.ts +101 -0
  14. package/dist/grounding/contract.js +138 -0
  15. package/dist/grounding/facts.d.ts +107 -0
  16. package/dist/grounding/facts.js +134 -0
  17. package/dist/grounding/index.d.ts +24 -0
  18. package/dist/grounding/index.js +24 -0
  19. package/dist/grounding/verify.d.ts +91 -0
  20. package/dist/grounding/verify.js +372 -0
  21. package/dist/health.d.ts +52 -0
  22. package/dist/health.js +64 -0
  23. package/dist/index.d.ts +50 -0
  24. package/dist/index.js +70 -0
  25. package/dist/limits.d.ts +102 -0
  26. package/dist/limits.js +136 -0
  27. package/dist/react.d.ts +8 -0
  28. package/dist/react.js +8 -0
  29. package/dist/registry.d.ts +133 -0
  30. package/dist/registry.js +126 -0
  31. package/dist/server.d.ts +10 -0
  32. package/dist/server.js +10 -0
  33. package/dist-cjs/grounding/contract.js +146 -0
  34. package/dist-cjs/grounding/facts.js +143 -0
  35. package/dist-cjs/grounding/index.js +43 -0
  36. package/dist-cjs/grounding/verify.js +376 -0
  37. package/dist-cjs/package.json +1 -0
  38. package/dist-cjs/registry.js +131 -0
  39. package/package.json +102 -0
  40. package/src/attempt.ts +82 -0
  41. package/src/catalog.ts +155 -0
  42. package/src/chain.ts +318 -0
  43. package/src/fair-share.ts +183 -0
  44. package/src/forms.ts +15 -0
  45. package/src/grounding/contract.ts +176 -0
  46. package/src/grounding/facts.ts +170 -0
  47. package/src/grounding/index.ts +50 -0
  48. package/src/grounding/verify.ts +429 -0
  49. package/src/health.ts +92 -0
  50. package/src/index.ts +124 -0
  51. package/src/limits.ts +137 -0
  52. package/src/react.ts +8 -0
  53. package/src/registry.ts +207 -0
  54. package/src/server.ts +10 -0
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NO_BASIS = void 0;
4
+ exports.buildContract = buildContract;
5
+ exports.buildAssistantRules = buildAssistantRules;
6
+ exports.directiveId = directiveId;
7
+ exports.renderDirectives = renderDirectives;
8
+ exports.buildGroundedContext = buildGroundedContext;
9
+ /**
10
+ * The grounding contract — the rules block that ships with every turn's facts.
11
+ * MIRRORED MODULE (see core/README.md).
12
+ *
13
+ * Why this is generated rather than a hand-written constant: a standing prose
14
+ * rule ("only use provided context") is a weak signal that models trade away
15
+ * under format pressure. The failure that motivated this harness was exactly
16
+ * that — a prompt demanding "1 focus, 3 tasks, 1 person, under 150 words, no
17
+ * hedging" got four confidently-formatted answers, three of them invented,
18
+ * against a context block that already said "if a question falls outside this
19
+ * context, say so rather than guessing".
20
+ *
21
+ * The lesson: the model did not disobey a rule it forgot. It obeyed the
22
+ * STRONGER of two conflicting instructions — fill five slots — because nothing
23
+ * made the empty slot expressible. So this block does three things a static
24
+ * prompt cannot:
25
+ *
26
+ * 1. Names the exact citation handles that exist this turn, so "cite a fact"
27
+ * is a closed-set choice rather than free text.
28
+ * 2. Names the exact fields that are unrecorded THIS TURN, so the prohibition
29
+ * is concrete ("you have no affiliation for any person here") instead of
30
+ * abstract.
31
+ * 3. Supplies the escape hatch verbatim, so refusing a slot is a cheaper
32
+ * token path than inventing one.
33
+ */
34
+ const facts_js_1 = require("./facts.js");
35
+ /** The exact string the model must emit when a slot cannot be filled. */
36
+ exports.NO_BASIS = "Not in your data.";
37
+ /**
38
+ * Build the contract for a specific fact set. Empty fact sets get the strictest
39
+ * form — with nothing retrieved, EVERY answer must be a refusal, and saying so
40
+ * plainly beats hoping the model notices the context block is empty.
41
+ */
42
+ function buildContract(facts, directives = []) {
43
+ const ids = [
44
+ ...facts.map((f) => `[${f.id}]`),
45
+ ...directives.map((_, i) => `[${directiveId(i)}]`),
46
+ ].join(" ");
47
+ const gaps = (0, facts_js_1.unrecordedFields)(facts);
48
+ const rules = [
49
+ "## Grounding contract — this overrides every formatting instruction below",
50
+ "",
51
+ "You are answering from a fixed set of records. They are the ONLY things you know about the operator.",
52
+ "",
53
+ facts.length === 0 && directives.length === 0
54
+ ? `1. NO records were retrieved for this turn. You therefore cannot answer any question about the operator's projects, people, goals, habits, commitments or events. Reply "${exports.NO_BASIS}" and say what you would need.`
55
+ : `1. Every claim about the operator MUST cite a record id. Legal citations this turn, and no others: ${ids}`,
56
+ `2. A field shown as \`${facts_js_1.NOT_RECORDED}\` means you DO NOT KNOW it. Never supply a value for it — not from the record's own wording, not from a name that looks like a place or an organisation, not from general knowledge about a similarly-named person. A surname is not an employer.`,
57
+ `3. If any part of the request has no supporting record, answer that part with exactly "${exports.NO_BASIS}" and continue with the parts you can support. A requested format NEVER obliges you to invent an item. Returning three of five requested items, each cited, is a correct and complete answer.`,
58
+ "4. Do not describe a person's role, employer, seniority, or history unless a record field states it. Do not infer an organisation from a name.",
59
+ "5. You have not browsed the web this turn. If asked to research someone, say you cannot and report only what the records hold.",
60
+ "6. If you are correcting an earlier answer, the correction is subject to every rule above — cite the record, or say the record does not exist.",
61
+ ];
62
+ if (gaps.length > 0) {
63
+ rules.push("", `Unrecorded in THIS turn's records — you have no value for any of these and must not state one: ${gaps.join(", ")}`);
64
+ }
65
+ return rules.join("\n");
66
+ }
67
+ /**
68
+ * The subset of the contract that needs no fact ids — for an assistant whose
69
+ * context is still prose (Cat) rather than typed records.
70
+ *
71
+ * Weaker than `buildContract` by construction: without ids there is nothing to
72
+ * cite, so rule 1 cannot exist and the verifier runs in entity-attribution
73
+ * mode. What survives is the part that stopped the worst failure — never state
74
+ * an attribute for someone in the user's data that their record does not carry,
75
+ * and never imply research you did not perform.
76
+ *
77
+ * This is a stepping stone, not the destination. It exists so a live product
78
+ * gets the protection now, without a same-day rewrite of its whole context
79
+ * layer; the destination is typed records here too.
80
+ */
81
+ function buildAssistantRules(opts) {
82
+ return [
83
+ "## Grounding rules — these override formatting instructions",
84
+ "",
85
+ `1. Everything you state about the user's own ${opts.subjectNoun} must come from the context above. Do not add an organisation, role, employer, history, or relationship that the context does not state.`,
86
+ "2. Do not infer an affiliation from a name. A word inside someone's name is not their employer or their city.",
87
+ "3. You have not browsed the web in this turn. If asked to research a person or company, say you cannot, and report only what the context holds.",
88
+ `4. If part of the request has no support in the context, answer that part with exactly "${exports.NO_BASIS}" and continue with the parts you can support. A requested format never obliges you to invent an item.`,
89
+ "5. General knowledge (how Bitcoin, Lightning, or a payment method works) is fine to use and is not covered by rules 1–2. The restriction is on facts about THIS user and the people and organisations in their data.",
90
+ "6. A correction is a claim too. If you are correcting yourself, it must be supported by the context or stated as unknown.",
91
+ ].join("\n");
92
+ }
93
+ /**
94
+ * Citation handle for a computed answer, parallel to a Fact's [F1].
95
+ *
96
+ * Directives used to be uncitable, and the contract demands a citation for
97
+ * every claim — so a model reporting a computed result had nothing legal to
98
+ * point at and wrote "[no record id]" into the user's answer. That is the
99
+ * harness leaking its own plumbing onto the screen. Give computed answers real
100
+ * ids and the sentence cites [D1] like anything else.
101
+ */
102
+ function directiveId(index) {
103
+ return `D${index + 1}`;
104
+ }
105
+ /**
106
+ * Render computed answers. These are stated as settled, because they are: the
107
+ * model must not re-derive, second-guess, or "improve" them, and an empty
108
+ * result must be reported as an empty result rather than backfilled from the
109
+ * fact set.
110
+ */
111
+ function renderDirectives(directives) {
112
+ if (directives.length === 0)
113
+ return "";
114
+ const blocks = directives.map((d, i) => {
115
+ const body = d.answer.length > 0
116
+ ? d.answer.map((a) => ` - ${a}`).join("\n")
117
+ : " (none — the query ran and matched nothing)";
118
+ return ` [${directiveId(i)}] ${d.question} [${d.method}]\n${body}`;
119
+ });
120
+ return [
121
+ "## Computed answers — already resolved, do not re-derive",
122
+ "These were computed directly from the database for this turn. They are exact.",
123
+ "Report them as given and cite their id, exactly as you would a record.",
124
+ "Where the result is empty, say so plainly — do not substitute a plausible item from the records.",
125
+ "",
126
+ ...blocks,
127
+ ].join("\n");
128
+ }
129
+ /**
130
+ * Assemble the full grounded context: contract, computed answers, then records.
131
+ *
132
+ * Order is deliberate and load-bearing. The contract comes FIRST so it frames
133
+ * everything read afterwards, and the records come LAST so they sit closest to
134
+ * the user's question — the position small models weight most heavily.
135
+ */
136
+ function buildGroundedContext(input) {
137
+ return [
138
+ buildContract(input.facts, input.directives ?? []),
139
+ renderDirectives(input.directives ?? []),
140
+ input.facts.length > 0
141
+ ? ["## Records", "", input.renderedFacts].join("\n")
142
+ : "## Records\n\n(none retrieved)",
143
+ ]
144
+ .filter(Boolean)
145
+ .join("\n\n---\n\n");
146
+ }
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Facts — the unit of grounded context. MIRRORED MODULE (see core/README.md).
4
+ *
5
+ * The problem this solves, concretely. Loki was asked who to contact and
6
+ * answered "Ilya Druzhnikov (UZH)". The stored record is:
7
+ *
8
+ * { displayName: "Ilya Druzhnikov", channels: { whatsapp: "+1650…" } }
9
+ *
10
+ * There is no org field, and the string "UZH" appears nowhere in the operator's
11
+ * data — it is the substring inside dr-UZH-nikov. A keyword match produced an
12
+ * affiliation out of a surname, and prose context gave the model no way to tell
13
+ * that "affiliation" was a field it had never been shown.
14
+ *
15
+ * The fix is representational, not a prompt instruction. A Fact is a RECORD with
16
+ * a DECLARED field set, and every declared field is rendered — including the ones
17
+ * with no value, which render as an explicit `<not recorded>`. A model that reads
18
+ *
19
+ * affiliation: <not recorded>
20
+ *
21
+ * is being told a specific negative, which is far harder to overwrite than the
22
+ * silence of a field that simply wasn't mentioned. Absence becomes evidence.
23
+ *
24
+ * Every fact also carries a short stable id ([F3]) so the answer can cite spans
25
+ * and `verify.ts` can check citations mechanically rather than by vibes.
26
+ *
27
+ * Pure: no DB, no network, no framework. Apps map their rows into Facts via
28
+ * their own adapters (FleetCrown: src/lib/agent/sources; OrangeCat: services/cat/sources).
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.FACT_KINDS = exports.NOT_RECORDED = void 0;
32
+ exports.declaredFields = declaredFields;
33
+ exports.makeFact = makeFact;
34
+ exports.assignFactIds = assignFactIds;
35
+ exports.factIds = factIds;
36
+ exports.renderFacts = renderFacts;
37
+ exports.unrecordedFields = unrecordedFields;
38
+ /** A field that is declared for a record kind but has no stored value. */
39
+ exports.NOT_RECORDED = "<not recorded>";
40
+ /**
41
+ * The declared field set per record kind — the SSOT for "what could be known
42
+ * about this kind of thing". Adding a field here makes it render as
43
+ * `<not recorded>` everywhere it is missing, which is the entire anti-invention
44
+ * mechanism: the model can only ever see fields we chose to declare.
45
+ *
46
+ * Deliberately includes fields we do NOT store (a person's `affiliation`,
47
+ * `role`, `employer`). That is not an oversight — those are exactly the
48
+ * attributes models invent, so naming them and marking them unrecorded is the
49
+ * point. Do not "clean up" this list by deleting the empty ones.
50
+ */
51
+ exports.FACT_KINDS = {
52
+ person: ["name", "affiliation", "role", "how_we_met", "last_interaction", "notes", "channels"],
53
+ project: ["name", "status", "stack", "description", "latest_dev_log", "repo"],
54
+ goal: ["title", "project", "progress", "target_date", "last_updated"],
55
+ habit: ["title", "frequency", "current_streak", "last_checked"],
56
+ commitment: ["title", "due", "counterparty", "status"],
57
+ event: ["name", "type", "deadline", "url", "status"],
58
+ // Humans the operator delegates to, and the work handed to them. Separate
59
+ // from `person`/`commitment` because the questions are different: a crew
60
+ // member is asked what they are good FOR, an assignment is asked who has it
61
+ // and whether they said yes.
62
+ crew_member: ["name", "role", "skills", "engagement", "rate", "availability", "open_assignments"],
63
+ assignment: ["title", "assignee", "status", "due", "fee", "why"],
64
+ document: ["title", "source", "excerpt"],
65
+ pending_action: ["title", "type", "reasoning", "proposed_on", "id"],
66
+ };
67
+ /** Field list for a kind; unknown kinds fall back to whatever the fact carries. */
68
+ function declaredFields(kind, fallback = []) {
69
+ return exports.FACT_KINDS[kind] ?? fallback;
70
+ }
71
+ /**
72
+ * Build a Fact with every declared field present. Values not supplied become
73
+ * null (→ `<not recorded>`). Undeclared keys are DROPPED rather than passed
74
+ * through: if a field is worth showing the model it is worth declaring in
75
+ * FACT_KINDS, otherwise the registry stops describing what the model sees.
76
+ */
77
+ function makeFact(input) {
78
+ const keys = declaredFields(input.kind, Object.keys(input.values ?? {}));
79
+ const fields = {};
80
+ for (const key of keys) {
81
+ const raw = input.values?.[key];
82
+ const trimmed = typeof raw === "string" ? raw.trim() : raw;
83
+ fields[key] = trimmed ? String(trimmed) : null;
84
+ }
85
+ return {
86
+ id: "",
87
+ kind: input.kind,
88
+ subject: input.subject,
89
+ source: input.source,
90
+ fields,
91
+ ...(input.similarity !== undefined ? { similarity: input.similarity } : {}),
92
+ };
93
+ }
94
+ /** Stamp sequential citation ids. Call once, after assembling the final set. */
95
+ function assignFactIds(facts) {
96
+ return facts.map((f, i) => ({ ...f, id: `F${i + 1}` }));
97
+ }
98
+ /** Every citation handle in a fact set — the only legal citations in an answer. */
99
+ function factIds(facts) {
100
+ return new Set(facts.map((f) => f.id));
101
+ }
102
+ /**
103
+ * Render facts for the model. One block per record, every declared field on its
104
+ * own line, unrecorded fields stated explicitly.
105
+ *
106
+ * [F3] person — Elena Weber SINGA Switzerland (people table)
107
+ * name: Elena Weber SINGA Switzerland
108
+ * affiliation: <not recorded>
109
+ * role: <not recorded>
110
+ * channels: whatsapp +41774730093
111
+ *
112
+ * The line-per-field shape matters for small models: a flat prose blob invites
113
+ * summarising (and summarising is where invention creeps in), whereas a field
114
+ * list invites lookup. Observed with 8B models — the same prompt over a blob
115
+ * hallucinates roles, over a field list it reports `<not recorded>`.
116
+ */
117
+ function renderFacts(facts) {
118
+ if (facts.length === 0)
119
+ return "";
120
+ return facts
121
+ .map((f) => {
122
+ const head = `[${f.id}] ${f.kind} — ${f.subject} (${f.source})`;
123
+ const body = Object.entries(f.fields).map(([k, v]) => ` ${k}: ${v ?? exports.NOT_RECORDED}`);
124
+ return [head, ...body].join("\n");
125
+ })
126
+ .join("\n\n");
127
+ }
128
+ /**
129
+ * Which declared fields are unrecorded across the set, as
130
+ * `kind.field` keys. The contract block names these explicitly so the rule
131
+ * "do not state an affiliation" is anchored to a concrete gap in THIS turn's
132
+ * context rather than being a standing abstraction the model may ignore.
133
+ */
134
+ function unrecordedFields(facts) {
135
+ const gaps = new Set();
136
+ for (const f of facts) {
137
+ for (const [k, v] of Object.entries(f.fields)) {
138
+ if (v === null)
139
+ gaps.add(`${f.kind}.${k}`);
140
+ }
141
+ }
142
+ return [...gaps].sort();
143
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildRepairPrompt = exports.verifyAnswer = exports.buildGroundedContext = exports.renderDirectives = exports.directiveId = exports.buildAssistantRules = exports.buildContract = exports.NO_BASIS = exports.unrecordedFields = exports.renderFacts = exports.factIds = exports.assignFactIds = exports.makeFact = exports.declaredFields = exports.FACT_KINDS = exports.NOT_RECORDED = void 0;
4
+ /**
5
+ * The grounding harness — imported, no longer mirrored.
6
+ *
7
+ * These three modules were born in FleetCrown (`src/lib/agent/core/`) and
8
+ * lived as a byte-identical mirror in OrangeCat, guarded by a SHA-256 drift
9
+ * check, because both assistants had the same failure: a model asked to fill
10
+ * a rigid answer format against thin context invents the missing parts, and
11
+ * the invention is indistinguishable from truth because both arrive as
12
+ * confident prose.
13
+ *
14
+ * The mirror's own README called the duplication "deliberate and temporary"
15
+ * and named this extraction as the exit. This is that exit: both apps now
16
+ * import `ai-kit/grounding`, and the drift check retires — two
17
+ * silently-diverging definitions of "what counts as grounded" are no longer
18
+ * possible, because there is only one.
19
+ *
20
+ * The constraint that made the code mirrorable is the constraint that makes
21
+ * it packageable, and it still holds: pure TypeScript, no DB, no network, no
22
+ * framework, no imports outside this directory. Anything that knows where
23
+ * data lives belongs in the app adapter that maps rows to `Fact`s, not here.
24
+ */
25
+ var facts_js_1 = require("./facts.js");
26
+ Object.defineProperty(exports, "NOT_RECORDED", { enumerable: true, get: function () { return facts_js_1.NOT_RECORDED; } });
27
+ Object.defineProperty(exports, "FACT_KINDS", { enumerable: true, get: function () { return facts_js_1.FACT_KINDS; } });
28
+ Object.defineProperty(exports, "declaredFields", { enumerable: true, get: function () { return facts_js_1.declaredFields; } });
29
+ Object.defineProperty(exports, "makeFact", { enumerable: true, get: function () { return facts_js_1.makeFact; } });
30
+ Object.defineProperty(exports, "assignFactIds", { enumerable: true, get: function () { return facts_js_1.assignFactIds; } });
31
+ Object.defineProperty(exports, "factIds", { enumerable: true, get: function () { return facts_js_1.factIds; } });
32
+ Object.defineProperty(exports, "renderFacts", { enumerable: true, get: function () { return facts_js_1.renderFacts; } });
33
+ Object.defineProperty(exports, "unrecordedFields", { enumerable: true, get: function () { return facts_js_1.unrecordedFields; } });
34
+ var contract_js_1 = require("./contract.js");
35
+ Object.defineProperty(exports, "NO_BASIS", { enumerable: true, get: function () { return contract_js_1.NO_BASIS; } });
36
+ Object.defineProperty(exports, "buildContract", { enumerable: true, get: function () { return contract_js_1.buildContract; } });
37
+ Object.defineProperty(exports, "buildAssistantRules", { enumerable: true, get: function () { return contract_js_1.buildAssistantRules; } });
38
+ Object.defineProperty(exports, "directiveId", { enumerable: true, get: function () { return contract_js_1.directiveId; } });
39
+ Object.defineProperty(exports, "renderDirectives", { enumerable: true, get: function () { return contract_js_1.renderDirectives; } });
40
+ Object.defineProperty(exports, "buildGroundedContext", { enumerable: true, get: function () { return contract_js_1.buildGroundedContext; } });
41
+ var verify_js_1 = require("./verify.js");
42
+ Object.defineProperty(exports, "verifyAnswer", { enumerable: true, get: function () { return verify_js_1.verifyAnswer; } });
43
+ Object.defineProperty(exports, "buildRepairPrompt", { enumerable: true, get: function () { return verify_js_1.buildRepairPrompt; } });