@tangleai/core 0.20.0
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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/package.json +62 -0
- package/src/clustering.d.ts +21 -0
- package/src/clustering.js +112 -0
- package/src/errors.d.ts +29 -0
- package/src/errors.js +36 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +6 -0
- package/src/schemas/memory.d.ts +128 -0
- package/src/schemas/memory.js +141 -0
- package/src/tokens.d.ts +14 -0
- package/src/tokens.js +23 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @tangleai/core
|
|
2
|
+
|
|
3
|
+
## 0.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Establish the coordinated 0.20.0 release with JavaScript and TypeScript declaration distributions, preserved public subpaths and JSON schemas, and verified Node and Bun consumers. Use published JarenJS 0.83.3 fixes without a consumer installation patch. Prepare versions before release commits, verify locally, push directly to main and publish CI-verified tarballs. Deploy the website independently from local Tangle workspace source with JarenJS packages from npm, verifying dependency sources and the live commit.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joham (jklarenbeek@gmail.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @tangleai/core
|
|
2
|
+
|
|
3
|
+
Tangle AI core — memory record model, k-means clustering, token heuristics (vector arithmetic is @jarenjs/core/vector's)
|
|
4
|
+
|
|
5
|
+
Install with `npm install @tangleai/core`. The npm distribution provides ESM JavaScript, TypeScript declarations, and the documented package subpaths for Node 24 and Bun 1.4 or newer.
|
|
6
|
+
|
|
7
|
+
See the [Tangle documentation](https://github.com/jklarenbeek/tangleai#readme) for architecture, examples, and runtime requirements. All public Tangle packages use one coordinated version.
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tangleai/core",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "Tangle AI core — memory record model, k-means clustering, token heuristics (vector arithmetic is @jarenjs/core/vector's)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.d.ts",
|
|
11
|
+
"import": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./errors": {
|
|
15
|
+
"types": "./src/errors.d.ts",
|
|
16
|
+
"import": "./src/errors.js",
|
|
17
|
+
"default": "./src/errors.js"
|
|
18
|
+
},
|
|
19
|
+
"./clustering": {
|
|
20
|
+
"types": "./src/clustering.d.ts",
|
|
21
|
+
"import": "./src/clustering.js",
|
|
22
|
+
"default": "./src/clustering.js"
|
|
23
|
+
},
|
|
24
|
+
"./tokens": {
|
|
25
|
+
"types": "./src/tokens.d.ts",
|
|
26
|
+
"import": "./src/tokens.js",
|
|
27
|
+
"default": "./src/tokens.js"
|
|
28
|
+
},
|
|
29
|
+
"./schemas/memory": {
|
|
30
|
+
"types": "./src/schemas/memory.d.ts",
|
|
31
|
+
"import": "./src/schemas/memory.js",
|
|
32
|
+
"default": "./src/schemas/memory.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@jarenjs/ai": "0.83.3"
|
|
42
|
+
},
|
|
43
|
+
"private": false,
|
|
44
|
+
"types": "./src/index.d.ts",
|
|
45
|
+
"files": [
|
|
46
|
+
"src/**/*.js",
|
|
47
|
+
"src/**/*.d.ts",
|
|
48
|
+
"schemas/**/*.json",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE",
|
|
51
|
+
"CHANGELOG.md"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public",
|
|
55
|
+
"registry": "https://registry.npmjs.org/"
|
|
56
|
+
},
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "git+https://github.com/jklarenbeek/tangleai.git",
|
|
60
|
+
"directory": "packages/core"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency centroid k-means with k-means++ seeding. Ported from
|
|
3
|
+
* memflow `src/utils/clustering.ts` with one deliberate change: the
|
|
4
|
+
* random source is INJECTED. memflow called `Math.random()` inline,
|
|
5
|
+
* which made every clustering run unreproducible — a test could assert
|
|
6
|
+
* "it converged" but never "it converged to this". Pass a seeded
|
|
7
|
+
* generator and the whole run is a pure function of its inputs.
|
|
8
|
+
*/
|
|
9
|
+
export interface KMeansResult {
|
|
10
|
+
/** Final centroid positions. */
|
|
11
|
+
centroids: number[][];
|
|
12
|
+
/** Cluster assignment for each input vector (index into centroids). */
|
|
13
|
+
assignments: number[];
|
|
14
|
+
/** Iterations until convergence. */
|
|
15
|
+
iterations: number;
|
|
16
|
+
}
|
|
17
|
+
export interface KMeansOptions {
|
|
18
|
+
maxIterations?: number;
|
|
19
|
+
random?: () => number;
|
|
20
|
+
}
|
|
21
|
+
export declare function kMeans(vectors: number[][], k: number, options?: KMeansOptions): KMeansResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency centroid k-means with k-means++ seeding. Ported from
|
|
3
|
+
* memflow `src/utils/clustering.ts` with one deliberate change: the
|
|
4
|
+
* random source is INJECTED. memflow called `Math.random()` inline,
|
|
5
|
+
* which made every clustering run unreproducible — a test could assert
|
|
6
|
+
* "it converged" but never "it converged to this". Pass a seeded
|
|
7
|
+
* generator and the whole run is a pure function of its inputs.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Squared Euclidean distance, private to this module. `@jarenjs/core/vector`
|
|
11
|
+
* is the suite's one home for vector arithmetic, but it publishes
|
|
12
|
+
* SIMILARITIES (higher-is-better, `1 / (1 + distance)` for Euclidean) and
|
|
13
|
+
* k-means++ seeding needs the squared distance itself; inverting the
|
|
14
|
+
* similarity back into a distance is a round trip nobody should have to
|
|
15
|
+
* read. Mismatched lengths answer Infinity — such a point joins no
|
|
16
|
+
* cluster rather than a wrong one.
|
|
17
|
+
*/
|
|
18
|
+
function squaredDistance(a, b) {
|
|
19
|
+
if (a.length !== b.length)
|
|
20
|
+
return Infinity;
|
|
21
|
+
let sum = 0;
|
|
22
|
+
for (let i = 0; i < a.length; i++) {
|
|
23
|
+
const d = a[i] - b[i];
|
|
24
|
+
sum += d * d;
|
|
25
|
+
}
|
|
26
|
+
return sum;
|
|
27
|
+
}
|
|
28
|
+
export function kMeans(vectors, k, options = {}) {
|
|
29
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
30
|
+
const random = options.random ?? Math.random;
|
|
31
|
+
if (vectors.length === 0 || k <= 0) {
|
|
32
|
+
return { centroids: [], assignments: [], iterations: 0 };
|
|
33
|
+
}
|
|
34
|
+
const n = vectors.length;
|
|
35
|
+
const dim = vectors[0].length;
|
|
36
|
+
const effectiveK = Math.min(k, n);
|
|
37
|
+
const centroids = initCentroids(vectors, effectiveK, random);
|
|
38
|
+
const assignments = new Array(n).fill(0);
|
|
39
|
+
let converged = false;
|
|
40
|
+
let iter = 0;
|
|
41
|
+
while (!converged && iter < maxIterations) {
|
|
42
|
+
iter++;
|
|
43
|
+
converged = true;
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
let minDist = Infinity;
|
|
46
|
+
let minIdx = 0;
|
|
47
|
+
for (let c = 0; c < effectiveK; c++) {
|
|
48
|
+
const dist = squaredDistance(vectors[i], centroids[c]);
|
|
49
|
+
if (dist < minDist) {
|
|
50
|
+
minDist = dist;
|
|
51
|
+
minIdx = c;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (assignments[i] !== minIdx) {
|
|
55
|
+
assignments[i] = minIdx;
|
|
56
|
+
converged = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (let c = 0; c < effectiveK; c++) {
|
|
60
|
+
const members = [];
|
|
61
|
+
for (let i = 0; i < n; i++)
|
|
62
|
+
if (assignments[i] === c)
|
|
63
|
+
members.push(vectors[i]);
|
|
64
|
+
if (members.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
for (let d = 0; d < dim; d++) {
|
|
67
|
+
let sum = 0;
|
|
68
|
+
for (const m of members)
|
|
69
|
+
sum += m[d];
|
|
70
|
+
centroids[c][d] = sum / members.length;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { centroids, assignments, iterations: iter };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* k-means++ seeding: first centroid uniform, the rest proportional to
|
|
78
|
+
* squared distance from the nearest chosen centroid.
|
|
79
|
+
*/
|
|
80
|
+
function initCentroids(vectors, k, random) {
|
|
81
|
+
const n = vectors.length;
|
|
82
|
+
const centroids = [];
|
|
83
|
+
centroids.push([...vectors[Math.floor(random() * n)]]);
|
|
84
|
+
for (let c = 1; c < k; c++) {
|
|
85
|
+
const distances = vectors.map((v) => {
|
|
86
|
+
let minDist = Infinity;
|
|
87
|
+
for (const cent of centroids) {
|
|
88
|
+
const d = squaredDistance(v, cent);
|
|
89
|
+
if (d < minDist)
|
|
90
|
+
minDist = d;
|
|
91
|
+
}
|
|
92
|
+
return minDist;
|
|
93
|
+
});
|
|
94
|
+
const totalDist = distances.reduce((a, b) => a + b, 0);
|
|
95
|
+
if (totalDist === 0) {
|
|
96
|
+
// remaining points coincide with existing centroids
|
|
97
|
+
centroids.push([...vectors[c % n]]);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
let threshold = random() * totalDist;
|
|
101
|
+
for (let i = 0; i < n; i++) {
|
|
102
|
+
threshold -= distances[i];
|
|
103
|
+
if (threshold <= 0) {
|
|
104
|
+
centroids.push([...vectors[i]]);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (centroids.length <= c)
|
|
109
|
+
centroids.push([...vectors[c % n]]);
|
|
110
|
+
}
|
|
111
|
+
return centroids;
|
|
112
|
+
}
|
package/src/errors.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tangle's error type. One class, coded, mirroring the jarenjs
|
|
3
|
+
* `CodedError` convention without importing it — @tangleai/core stays
|
|
4
|
+
* dependency-free so the vector math and schemas can be lifted anywhere.
|
|
5
|
+
*
|
|
6
|
+
* Codes:
|
|
7
|
+
* TA0001 — caller/config error (bad argument, missing seam)
|
|
8
|
+
* TA0002 — transport error (HTTP status from a provider)
|
|
9
|
+
* TA0003 — malformed payload (a provider answered, but not with what
|
|
10
|
+
* the contract promises)
|
|
11
|
+
*
|
|
12
|
+
* The split matters the same way it does in @jarenjs/ai: TA0001 is a bug
|
|
13
|
+
* in the host, TA0002 is the network's fault, TA0003 is the provider's.
|
|
14
|
+
* A retry policy may retry the second, must not retry the first, and
|
|
15
|
+
* should log the third.
|
|
16
|
+
*/
|
|
17
|
+
export type TangleErrorCode = 'TA0001' | 'TA0002' | 'TA0003';
|
|
18
|
+
export interface TangleErrorOptions {
|
|
19
|
+
status?: number;
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
export declare class TangleError extends Error {
|
|
23
|
+
readonly code: TangleErrorCode;
|
|
24
|
+
readonly status?: number;
|
|
25
|
+
constructor(code: TangleErrorCode, message: string, options?: TangleErrorOptions);
|
|
26
|
+
}
|
|
27
|
+
export declare function callerError(message: string, options?: TangleErrorOptions): TangleError;
|
|
28
|
+
export declare function transportError(message: string, options?: TangleErrorOptions): TangleError;
|
|
29
|
+
export declare function payloadError(message: string, options?: TangleErrorOptions): TangleError;
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tangle's error type. One class, coded, mirroring the jarenjs
|
|
3
|
+
* `CodedError` convention without importing it — @tangleai/core stays
|
|
4
|
+
* dependency-free so the vector math and schemas can be lifted anywhere.
|
|
5
|
+
*
|
|
6
|
+
* Codes:
|
|
7
|
+
* TA0001 — caller/config error (bad argument, missing seam)
|
|
8
|
+
* TA0002 — transport error (HTTP status from a provider)
|
|
9
|
+
* TA0003 — malformed payload (a provider answered, but not with what
|
|
10
|
+
* the contract promises)
|
|
11
|
+
*
|
|
12
|
+
* The split matters the same way it does in @jarenjs/ai: TA0001 is a bug
|
|
13
|
+
* in the host, TA0002 is the network's fault, TA0003 is the provider's.
|
|
14
|
+
* A retry policy may retry the second, must not retry the first, and
|
|
15
|
+
* should log the third.
|
|
16
|
+
*/
|
|
17
|
+
export class TangleError extends Error {
|
|
18
|
+
code;
|
|
19
|
+
status;
|
|
20
|
+
constructor(code, message, options = {}) {
|
|
21
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
22
|
+
this.name = 'TangleError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
if (options.status !== undefined)
|
|
25
|
+
this.status = options.status;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function callerError(message, options) {
|
|
29
|
+
return new TangleError('TA0001', message, options);
|
|
30
|
+
}
|
|
31
|
+
export function transportError(message, options) {
|
|
32
|
+
return new TangleError('TA0002', message, options);
|
|
33
|
+
}
|
|
34
|
+
export function payloadError(message, options) {
|
|
35
|
+
return new TangleError('TA0003', message, options);
|
|
36
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** @tangleai/core barrel. Subpath exports exist for tree-shaking hosts;
|
|
2
|
+
* this re-export exists for convenience. */
|
|
3
|
+
export { TangleError, callerError, transportError, payloadError } from './errors.ts';
|
|
4
|
+
export type { TangleErrorCode, TangleErrorOptions } from './errors.ts';
|
|
5
|
+
export { kMeans } from './clustering.ts';
|
|
6
|
+
export type { KMeansResult, KMeansOptions } from './clustering.ts';
|
|
7
|
+
export { CHARS_PER_TOKEN, estimateTokens, truncateToTokens } from './tokens.ts';
|
|
8
|
+
export { MEMORY_KINDS, MEMORY_UNIT_SCHEMA, MEMORY_RELATION_SCHEMA, OUTCOME_REPORT_SCHEMA, MEMORY_SCHEMAS, toLedgerMemory, } from './schemas/memory.ts';
|
|
9
|
+
export type { JsonSchema, MemoryKind, MemoryRelation, EmbeddedBy, MemoryUnit, OutcomeReport, LedgerMemory, } from './schemas/memory.ts';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** @tangleai/core barrel. Subpath exports exist for tree-shaking hosts;
|
|
2
|
+
* this re-export exists for convenience. */
|
|
3
|
+
export { TangleError, callerError, transportError, payloadError } from "./errors.js";
|
|
4
|
+
export { kMeans } from "./clustering.js";
|
|
5
|
+
export { CHARS_PER_TOKEN, estimateTokens, truncateToTokens } from "./tokens.js";
|
|
6
|
+
export { MEMORY_KINDS, MEMORY_UNIT_SCHEMA, MEMORY_RELATION_SCHEMA, OUTCOME_REPORT_SCHEMA, MEMORY_SCHEMAS, toLedgerMemory, } from "./schemas/memory.js";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tangle memory record, as plain JSON Schema — memflow's `MemoryUnit`
|
|
3
|
+
* aligned with the text-evidence specialization of the @jarenjs/ai ledger memory.
|
|
4
|
+
*
|
|
5
|
+
* The alignment is the design decision that matters. A jarenjs ledger
|
|
6
|
+
* memory is `{ id, text, evidence, tags, at }` with evidence REQUIRED —
|
|
7
|
+
* a memory without evidence is a guess. memflow's MemoryUnit had
|
|
8
|
+
* `content`/`timestamp`/`metadata` and no evidence rule, which is one
|
|
9
|
+
* reason its consolidation loops could never be audited. Tangle keeps
|
|
10
|
+
* jarenjs's five fields under jarenjs's names and adds what the
|
|
11
|
+
* consolidation policies need on top:
|
|
12
|
+
*
|
|
13
|
+
* kind — fact | event | summary | relation (memflow's `type`)
|
|
14
|
+
* embedding — the vector, present once an embed pass has run, and
|
|
15
|
+
* embeddedBy — its identity `{ model, dims }`, the jarenjs ledger's
|
|
16
|
+
* own rule (a vector never travels without the model
|
|
17
|
+
* that made it: vectors from two models compare into
|
|
18
|
+
* plausible garbage). Both-or-neither, as in the ledger.
|
|
19
|
+
* confidence — [0,1], moved by outcome learning, never below the floor
|
|
20
|
+
* supersededBy / supersededAt / supersededReason — contradiction
|
|
21
|
+
* resolution marks the loser instead of deleting it, so
|
|
22
|
+
* "what did we believe before" stays answerable
|
|
23
|
+
* mergedFrom — crystallization provenance: the ids this record absorbed
|
|
24
|
+
* relations — typed edges to other memories, document-shaped; a graph
|
|
25
|
+
* store may index these later, nothing requires one now
|
|
26
|
+
*
|
|
27
|
+
* `toLedgerMemory()` projects a Tangle record down to the jarenjs fields
|
|
28
|
+
* — the five the ledger always held plus the optional embedding pair —
|
|
29
|
+
* so any Tangle memory can be mirrored into a real ledger and recalled
|
|
30
|
+
* by an unmodified @jarenjs/ai agent, by tag AND by meaning
|
|
31
|
+
* (`recall({ near })` through the same embedder that wrote the vector).
|
|
32
|
+
* The projection is lossy by design — the ledger's strictness
|
|
33
|
+
* (`additionalProperties: false`) is a feature we align with, not a
|
|
34
|
+
* limitation we fight.
|
|
35
|
+
*
|
|
36
|
+
* The interfaces are hand-written beside the schemas rather than derived
|
|
37
|
+
* from them: the schemas are the runtime contract (what a validator
|
|
38
|
+
* enforces at the store boundary), the interfaces are the compile-time
|
|
39
|
+
* one, and the schema tests are what keep them honest with each other.
|
|
40
|
+
*/
|
|
41
|
+
/** A JSON Schema document as a plain map: this module has no runtime imports
|
|
42
|
+
* and does not name `@jarenjs/validate`'s `JSONSchema` type. The
|
|
43
|
+
* validator's boundary methods take `JSONSchemaLike` (`JSONSchema |
|
|
44
|
+
* Record<string, unknown>`), so a plain map crosses without a cast. */
|
|
45
|
+
export type JsonSchema = Record<string, unknown>;
|
|
46
|
+
export declare const MEMORY_KINDS: readonly ["fact", "event", "summary", "relation"];
|
|
47
|
+
export type MemoryKind = typeof MEMORY_KINDS[number];
|
|
48
|
+
export interface MemoryRelation {
|
|
49
|
+
target: string;
|
|
50
|
+
relType: string;
|
|
51
|
+
weight?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Which model produced a vector, at what width — the same shape as
|
|
54
|
+
* `LedgerEmbeddedBy` in `@jarenjs/ai/schemas/ledger`, restated here
|
|
55
|
+
* because this package is dependency-free (the mirror test pins the
|
|
56
|
+
* two against each other). */
|
|
57
|
+
export interface EmbeddedBy {
|
|
58
|
+
model: string;
|
|
59
|
+
dims: number;
|
|
60
|
+
}
|
|
61
|
+
export interface MemoryUnit {
|
|
62
|
+
id: string;
|
|
63
|
+
text: string;
|
|
64
|
+
evidence: string;
|
|
65
|
+
tags: string[];
|
|
66
|
+
/** RFC 3339. */
|
|
67
|
+
at: string;
|
|
68
|
+
kind: MemoryKind;
|
|
69
|
+
/** Present exactly when `embeddedBy` is. */
|
|
70
|
+
embedding?: number[];
|
|
71
|
+
/** Present exactly when `embedding` is. */
|
|
72
|
+
embeddedBy?: EmbeddedBy;
|
|
73
|
+
confidence?: number;
|
|
74
|
+
supersededBy?: string;
|
|
75
|
+
supersededAt?: string;
|
|
76
|
+
supersededReason?: string;
|
|
77
|
+
mergedFrom?: string[];
|
|
78
|
+
relations?: MemoryRelation[];
|
|
79
|
+
}
|
|
80
|
+
export interface OutcomeReport {
|
|
81
|
+
memoryIds: string[];
|
|
82
|
+
outcome: 'success' | 'failure' | 'partial';
|
|
83
|
+
/** RFC 3339. */
|
|
84
|
+
at: string;
|
|
85
|
+
/** What happened, in the world — the ground truth that makes the
|
|
86
|
+
* adjustment an observation rather than a self-judgment. */
|
|
87
|
+
evidence: string;
|
|
88
|
+
}
|
|
89
|
+
/** The vector pair, both-or-neither: the schema's `dependencies` rule
|
|
90
|
+
* (`EMBEDDING_PAIR`) stated in the type. Narrow on either member and
|
|
91
|
+
* the other follows; an orphan vector does not type. */
|
|
92
|
+
export type EmbeddingPair = {
|
|
93
|
+
embedding?: undefined;
|
|
94
|
+
embeddedBy?: undefined;
|
|
95
|
+
} | {
|
|
96
|
+
embedding: number[];
|
|
97
|
+
embeddedBy: EmbeddedBy;
|
|
98
|
+
};
|
|
99
|
+
/** A @jarenjs/ai ledger memory's own five fields. */
|
|
100
|
+
export interface LedgerMemoryFields {
|
|
101
|
+
id: string;
|
|
102
|
+
text: string;
|
|
103
|
+
evidence: string;
|
|
104
|
+
tags: string[];
|
|
105
|
+
at: string;
|
|
106
|
+
}
|
|
107
|
+
/** What a @jarenjs/ai ledger memory holds: the five fields, plus the
|
|
108
|
+
* vector pair. Structurally identical to `LedgerMemory` in
|
|
109
|
+
* `@jarenjs/ai/schemas/ledger` — restated because this package is
|
|
110
|
+
* dependency-free; `test/memory/ledger-mirror.test.ts` pins the two
|
|
111
|
+
* against each other at compile time, in both directions. */
|
|
112
|
+
export type LedgerMemory = import('@jarenjs/ai/schemas/ledger').LedgerMemory;
|
|
113
|
+
export declare const MEMORY_RELATION_SCHEMA: JsonSchema;
|
|
114
|
+
export declare const MEMORY_UNIT_SCHEMA: JsonSchema;
|
|
115
|
+
/** The outcome report a host files after acting on recalled memories. */
|
|
116
|
+
export declare const OUTCOME_REPORT_SCHEMA: JsonSchema;
|
|
117
|
+
export declare const MEMORY_SCHEMAS: {
|
|
118
|
+
readonly unit: JsonSchema;
|
|
119
|
+
readonly relation: JsonSchema;
|
|
120
|
+
readonly outcome: JsonSchema;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Project a Tangle memory down to what a @jarenjs/ai ledger accepts:
|
|
124
|
+
* the five fields, and the embedding pair when the unit carries one —
|
|
125
|
+
* so a mirrored memory is recallable by meaning, not only by tag.
|
|
126
|
+
* Lossy on purpose; see the header.
|
|
127
|
+
*/
|
|
128
|
+
export declare function toLedgerMemory(unit: MemoryUnit): LedgerMemory;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tangle memory record, as plain JSON Schema — memflow's `MemoryUnit`
|
|
3
|
+
* aligned with the text-evidence specialization of the @jarenjs/ai ledger memory.
|
|
4
|
+
*
|
|
5
|
+
* The alignment is the design decision that matters. A jarenjs ledger
|
|
6
|
+
* memory is `{ id, text, evidence, tags, at }` with evidence REQUIRED —
|
|
7
|
+
* a memory without evidence is a guess. memflow's MemoryUnit had
|
|
8
|
+
* `content`/`timestamp`/`metadata` and no evidence rule, which is one
|
|
9
|
+
* reason its consolidation loops could never be audited. Tangle keeps
|
|
10
|
+
* jarenjs's five fields under jarenjs's names and adds what the
|
|
11
|
+
* consolidation policies need on top:
|
|
12
|
+
*
|
|
13
|
+
* kind — fact | event | summary | relation (memflow's `type`)
|
|
14
|
+
* embedding — the vector, present once an embed pass has run, and
|
|
15
|
+
* embeddedBy — its identity `{ model, dims }`, the jarenjs ledger's
|
|
16
|
+
* own rule (a vector never travels without the model
|
|
17
|
+
* that made it: vectors from two models compare into
|
|
18
|
+
* plausible garbage). Both-or-neither, as in the ledger.
|
|
19
|
+
* confidence — [0,1], moved by outcome learning, never below the floor
|
|
20
|
+
* supersededBy / supersededAt / supersededReason — contradiction
|
|
21
|
+
* resolution marks the loser instead of deleting it, so
|
|
22
|
+
* "what did we believe before" stays answerable
|
|
23
|
+
* mergedFrom — crystallization provenance: the ids this record absorbed
|
|
24
|
+
* relations — typed edges to other memories, document-shaped; a graph
|
|
25
|
+
* store may index these later, nothing requires one now
|
|
26
|
+
*
|
|
27
|
+
* `toLedgerMemory()` projects a Tangle record down to the jarenjs fields
|
|
28
|
+
* — the five the ledger always held plus the optional embedding pair —
|
|
29
|
+
* so any Tangle memory can be mirrored into a real ledger and recalled
|
|
30
|
+
* by an unmodified @jarenjs/ai agent, by tag AND by meaning
|
|
31
|
+
* (`recall({ near })` through the same embedder that wrote the vector).
|
|
32
|
+
* The projection is lossy by design — the ledger's strictness
|
|
33
|
+
* (`additionalProperties: false`) is a feature we align with, not a
|
|
34
|
+
* limitation we fight.
|
|
35
|
+
*
|
|
36
|
+
* The interfaces are hand-written beside the schemas rather than derived
|
|
37
|
+
* from them: the schemas are the runtime contract (what a validator
|
|
38
|
+
* enforces at the store boundary), the interfaces are the compile-time
|
|
39
|
+
* one, and the schema tests are what keep them honest with each other.
|
|
40
|
+
*/
|
|
41
|
+
/** RFC 3339 timestamp — same shape and same reasoning as the jarenjs ledger:
|
|
42
|
+
* `format` is honest metadata, `pattern` is the zero-dependency enforcement,
|
|
43
|
+
* and recency sorts these strings lexicographically. */
|
|
44
|
+
const AT = {
|
|
45
|
+
type: 'string',
|
|
46
|
+
format: 'date-time',
|
|
47
|
+
pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$',
|
|
48
|
+
};
|
|
49
|
+
/** A non-empty identifier. */
|
|
50
|
+
const ID = { type: 'string', minLength: 1 };
|
|
51
|
+
/** A stored embedding: plain numbers, never a typed array — a
|
|
52
|
+
* `Float32Array` does not survive JSON, and the store boundary is JSON.
|
|
53
|
+
* Finiteness and width are the identity's job, checked where the vector
|
|
54
|
+
* is used (`@jarenjs/core/vector` `isVector`). */
|
|
55
|
+
const EMBEDDING = { type: 'array', items: { type: 'number' }, minItems: 1 };
|
|
56
|
+
/** A vector's identity, byte-for-byte the jarenjs ledger's `EMBEDDED_BY`. */
|
|
57
|
+
const EMBEDDED_BY = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
model: { type: 'string', minLength: 1 },
|
|
61
|
+
dims: { type: 'integer', minimum: 1 },
|
|
62
|
+
},
|
|
63
|
+
required: ['model', 'dims'],
|
|
64
|
+
additionalProperties: false,
|
|
65
|
+
};
|
|
66
|
+
/** Both-or-neither, spelled as draft-07 `dependencies` — the ledger's
|
|
67
|
+
* spelling, under the validator's draft-07 default. */
|
|
68
|
+
const EMBEDDING_PAIR = { embedding: ['embeddedBy'], embeddedBy: ['embedding'] };
|
|
69
|
+
export const MEMORY_KINDS = ['fact', 'event', 'summary', 'relation'];
|
|
70
|
+
export const MEMORY_RELATION_SCHEMA = {
|
|
71
|
+
$id: 'https://tangleai.dev/schemas/memory-relation.json',
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
target: ID,
|
|
75
|
+
relType: { type: 'string', minLength: 1 },
|
|
76
|
+
weight: { type: 'number', minimum: 0 },
|
|
77
|
+
},
|
|
78
|
+
required: ['target', 'relType'],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
};
|
|
81
|
+
export const MEMORY_UNIT_SCHEMA = {
|
|
82
|
+
$id: 'https://tangleai.dev/schemas/memory-unit.json',
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
id: ID,
|
|
86
|
+
text: { type: 'string', minLength: 1 },
|
|
87
|
+
evidence: { type: 'string', minLength: 1 },
|
|
88
|
+
tags: { type: 'array', items: { type: 'string', minLength: 1 } },
|
|
89
|
+
at: AT,
|
|
90
|
+
kind: { enum: [...MEMORY_KINDS] },
|
|
91
|
+
embedding: EMBEDDING,
|
|
92
|
+
embeddedBy: EMBEDDED_BY,
|
|
93
|
+
confidence: { type: 'number', minimum: 0, maximum: 1 },
|
|
94
|
+
supersededBy: ID,
|
|
95
|
+
supersededAt: AT,
|
|
96
|
+
supersededReason: { type: 'string', minLength: 1 },
|
|
97
|
+
mergedFrom: { type: 'array', items: ID },
|
|
98
|
+
relations: { type: 'array', items: { $ref: 'https://tangleai.dev/schemas/memory-relation.json' } },
|
|
99
|
+
},
|
|
100
|
+
required: ['id', 'text', 'evidence', 'tags', 'at', 'kind'],
|
|
101
|
+
dependencies: EMBEDDING_PAIR,
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
};
|
|
104
|
+
/** The outcome report a host files after acting on recalled memories. */
|
|
105
|
+
export const OUTCOME_REPORT_SCHEMA = {
|
|
106
|
+
$id: 'https://tangleai.dev/schemas/outcome-report.json',
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
memoryIds: { type: 'array', items: ID, minItems: 1 },
|
|
110
|
+
outcome: { enum: ['success', 'failure', 'partial'] },
|
|
111
|
+
at: AT,
|
|
112
|
+
// what happened, in the world — the ground truth that makes the
|
|
113
|
+
// adjustment an observation rather than a self-judgment
|
|
114
|
+
evidence: { type: 'string', minLength: 1 },
|
|
115
|
+
},
|
|
116
|
+
required: ['memoryIds', 'outcome', 'at', 'evidence'],
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
};
|
|
119
|
+
export const MEMORY_SCHEMAS = {
|
|
120
|
+
unit: MEMORY_UNIT_SCHEMA,
|
|
121
|
+
relation: MEMORY_RELATION_SCHEMA,
|
|
122
|
+
outcome: OUTCOME_REPORT_SCHEMA,
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Project a Tangle memory down to what a @jarenjs/ai ledger accepts:
|
|
126
|
+
* the five fields, and the embedding pair when the unit carries one —
|
|
127
|
+
* so a mirrored memory is recallable by meaning, not only by tag.
|
|
128
|
+
* Lossy on purpose; see the header.
|
|
129
|
+
*/
|
|
130
|
+
export function toLedgerMemory(unit) {
|
|
131
|
+
const fields = {
|
|
132
|
+
id: unit.id,
|
|
133
|
+
text: unit.text,
|
|
134
|
+
evidence: unit.evidence,
|
|
135
|
+
tags: unit.tags,
|
|
136
|
+
at: unit.at,
|
|
137
|
+
};
|
|
138
|
+
return unit.embedding !== undefined && unit.embeddedBy !== undefined
|
|
139
|
+
? { ...fields, embedding: unit.embedding, embeddedBy: { ...unit.embeddedBy } }
|
|
140
|
+
: fields;
|
|
141
|
+
}
|
package/src/tokens.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimation. The 4-characters-per-token heuristic from memflow
|
|
3
|
+
* `src/utils/tokens.ts`, kept as a heuristic ON PURPOSE: a real
|
|
4
|
+
* tokenizer is model-specific and heavy, and every place this number is
|
|
5
|
+
* used (budgets, truncation) already treats it as a ceiling, not a
|
|
6
|
+
* measurement. When a budget decision starts to matter more than ±25%,
|
|
7
|
+
* the fix is a real count from the provider's usage field, not a better
|
|
8
|
+
* guess here.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CHARS_PER_TOKEN = 4;
|
|
11
|
+
/** Estimated token count, never negative. */
|
|
12
|
+
export declare function estimateTokens(text: string): number;
|
|
13
|
+
/** Truncate `text` so its estimated token count fits `maxTokens`. */
|
|
14
|
+
export declare function truncateToTokens(text: string, maxTokens: number): string;
|
package/src/tokens.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token estimation. The 4-characters-per-token heuristic from memflow
|
|
3
|
+
* `src/utils/tokens.ts`, kept as a heuristic ON PURPOSE: a real
|
|
4
|
+
* tokenizer is model-specific and heavy, and every place this number is
|
|
5
|
+
* used (budgets, truncation) already treats it as a ceiling, not a
|
|
6
|
+
* measurement. When a budget decision starts to matter more than ±25%,
|
|
7
|
+
* the fix is a real count from the provider's usage field, not a better
|
|
8
|
+
* guess here.
|
|
9
|
+
*/
|
|
10
|
+
export const CHARS_PER_TOKEN = 4;
|
|
11
|
+
/** Estimated token count, never negative. */
|
|
12
|
+
export function estimateTokens(text) {
|
|
13
|
+
if (!text)
|
|
14
|
+
return 0;
|
|
15
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
16
|
+
}
|
|
17
|
+
/** Truncate `text` so its estimated token count fits `maxTokens`. */
|
|
18
|
+
export function truncateToTokens(text, maxTokens) {
|
|
19
|
+
if (!text || maxTokens <= 0)
|
|
20
|
+
return '';
|
|
21
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
22
|
+
return text.length <= maxChars ? text : text.slice(0, maxChars);
|
|
23
|
+
}
|