@tangleai/pipeline 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 +13 -0
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/package.json +67 -0
- package/src/dag.d.ts +59 -0
- package/src/dag.js +44 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +5 -0
- package/src/mermaid.d.ts +12 -0
- package/src/mermaid.js +19 -0
- package/src/run.d.ts +80 -0
- package/src/run.js +108 -0
- package/src/standins.d.ts +41 -0
- package/src/standins.js +47 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @tangleai/pipeline
|
|
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.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies
|
|
12
|
+
- @tangleai/core@0.20.0
|
|
13
|
+
- @tangleai/memory@0.20.0
|
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/pipeline
|
|
2
|
+
|
|
3
|
+
Tangle AI pipeline — the memory loop as a jaren-dag document, executed by @jarenjs/flow, projected to mermaid for display
|
|
4
|
+
|
|
5
|
+
Install with `npm install @tangleai/pipeline`. 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,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tangleai/pipeline",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"description": "Tangle AI pipeline — the memory loop as a jaren-dag document, executed by @jarenjs/flow, projected to mermaid for display",
|
|
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
|
+
"./dag": {
|
|
15
|
+
"types": "./src/dag.d.ts",
|
|
16
|
+
"import": "./src/dag.js",
|
|
17
|
+
"default": "./src/dag.js"
|
|
18
|
+
},
|
|
19
|
+
"./run": {
|
|
20
|
+
"types": "./src/run.d.ts",
|
|
21
|
+
"import": "./src/run.js",
|
|
22
|
+
"default": "./src/run.js"
|
|
23
|
+
},
|
|
24
|
+
"./mermaid": {
|
|
25
|
+
"types": "./src/mermaid.d.ts",
|
|
26
|
+
"import": "./src/mermaid.js",
|
|
27
|
+
"default": "./src/mermaid.js"
|
|
28
|
+
},
|
|
29
|
+
"./standins": {
|
|
30
|
+
"types": "./src/standins.d.ts",
|
|
31
|
+
"import": "./src/standins.js",
|
|
32
|
+
"default": "./src/standins.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@tangleai/core": "^0.20.0",
|
|
42
|
+
"@tangleai/memory": "^0.20.0",
|
|
43
|
+
"@jarenjs/ai": "0.83.3",
|
|
44
|
+
"@jarenjs/flow": "0.83.3",
|
|
45
|
+
"@jarenjs/json": "0.83.3",
|
|
46
|
+
"@jarenjs/mermaid": "0.83.3"
|
|
47
|
+
},
|
|
48
|
+
"private": false,
|
|
49
|
+
"types": "./src/index.d.ts",
|
|
50
|
+
"files": [
|
|
51
|
+
"src/**/*.js",
|
|
52
|
+
"src/**/*.d.ts",
|
|
53
|
+
"schemas/**/*.json",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE",
|
|
56
|
+
"CHANGELOG.md"
|
|
57
|
+
],
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public",
|
|
60
|
+
"registry": "https://registry.npmjs.org/"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "git+https://github.com/jklarenbeek/tangleai.git",
|
|
65
|
+
"directory": "packages/pipeline"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/dag.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tangle memory loop as a `jaren-dag` 0.1 document.
|
|
3
|
+
*
|
|
4
|
+
* This is the SAME loop the walking skeleton runs inline, lifted into a
|
|
5
|
+
* flow document so that (a) @jarenjs/flow executes it with real
|
|
6
|
+
* per-node observability (`onNode` → the run log → the desktop DAG
|
|
7
|
+
* page, current and historical), and (b) @jarenjs/mermaid can project
|
|
8
|
+
* the topology for display from the document itself — the picture the
|
|
9
|
+
* user sees IS the thing that runs, not an illustration of it.
|
|
10
|
+
*
|
|
11
|
+
* Stage order is the twice-learned design rule: the novelty gate only
|
|
12
|
+
* filters near-verbatim repeats, contradictions are resolved BEFORE
|
|
13
|
+
* crystallization (a contradiction is ~0.95-similar to what it
|
|
14
|
+
* contradicts), and the crystallizer merges only what survived with its
|
|
15
|
+
* meaning intact.
|
|
16
|
+
*
|
|
17
|
+
* Each stage hands its summary to the `report` output node through a
|
|
18
|
+
* ported edge; the unported spine carries the data each next stage
|
|
19
|
+
* needs. Handlers are injected at compile time (`createPipeline`) — the
|
|
20
|
+
* document names them, the host provides them, jarenjs's DI rule.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PIPELINE_DAG: {
|
|
23
|
+
$dag: string;
|
|
24
|
+
nodes: {
|
|
25
|
+
observations: {
|
|
26
|
+
kind: string;
|
|
27
|
+
};
|
|
28
|
+
embed: {
|
|
29
|
+
kind: string;
|
|
30
|
+
run: string;
|
|
31
|
+
};
|
|
32
|
+
novelty: {
|
|
33
|
+
kind: string;
|
|
34
|
+
run: string;
|
|
35
|
+
};
|
|
36
|
+
contradiction: {
|
|
37
|
+
kind: string;
|
|
38
|
+
run: string;
|
|
39
|
+
};
|
|
40
|
+
crystallize: {
|
|
41
|
+
kind: string;
|
|
42
|
+
run: string;
|
|
43
|
+
};
|
|
44
|
+
report: {
|
|
45
|
+
kind: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
edges: ({
|
|
49
|
+
from: string;
|
|
50
|
+
to: string;
|
|
51
|
+
port?: undefined;
|
|
52
|
+
} | {
|
|
53
|
+
from: string;
|
|
54
|
+
to: string;
|
|
55
|
+
port: string;
|
|
56
|
+
})[];
|
|
57
|
+
};
|
|
58
|
+
/** The node ids, in pipeline order — what the UI renders as the status strip. */
|
|
59
|
+
export declare const PIPELINE_NODES: readonly ["observations", "embed", "novelty", "contradiction", "crystallize", "report"];
|
package/src/dag.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tangle memory loop as a `jaren-dag` 0.1 document.
|
|
3
|
+
*
|
|
4
|
+
* This is the SAME loop the walking skeleton runs inline, lifted into a
|
|
5
|
+
* flow document so that (a) @jarenjs/flow executes it with real
|
|
6
|
+
* per-node observability (`onNode` → the run log → the desktop DAG
|
|
7
|
+
* page, current and historical), and (b) @jarenjs/mermaid can project
|
|
8
|
+
* the topology for display from the document itself — the picture the
|
|
9
|
+
* user sees IS the thing that runs, not an illustration of it.
|
|
10
|
+
*
|
|
11
|
+
* Stage order is the twice-learned design rule: the novelty gate only
|
|
12
|
+
* filters near-verbatim repeats, contradictions are resolved BEFORE
|
|
13
|
+
* crystallization (a contradiction is ~0.95-similar to what it
|
|
14
|
+
* contradicts), and the crystallizer merges only what survived with its
|
|
15
|
+
* meaning intact.
|
|
16
|
+
*
|
|
17
|
+
* Each stage hands its summary to the `report` output node through a
|
|
18
|
+
* ported edge; the unported spine carries the data each next stage
|
|
19
|
+
* needs. Handlers are injected at compile time (`createPipeline`) — the
|
|
20
|
+
* document names them, the host provides them, jarenjs's DI rule.
|
|
21
|
+
*/
|
|
22
|
+
export const PIPELINE_DAG = {
|
|
23
|
+
$dag: '0.1',
|
|
24
|
+
nodes: {
|
|
25
|
+
observations: { kind: 'input' },
|
|
26
|
+
embed: { kind: 'task', run: 'embed' },
|
|
27
|
+
novelty: { kind: 'task', run: 'novelty' },
|
|
28
|
+
contradiction: { kind: 'task', run: 'contradiction' },
|
|
29
|
+
crystallize: { kind: 'task', run: 'crystallize' },
|
|
30
|
+
report: { kind: 'output' },
|
|
31
|
+
},
|
|
32
|
+
edges: [
|
|
33
|
+
{ from: 'observations', to: 'embed' },
|
|
34
|
+
{ from: 'embed', to: 'novelty' },
|
|
35
|
+
{ from: 'novelty', to: 'contradiction' },
|
|
36
|
+
{ from: 'contradiction', to: 'crystallize' },
|
|
37
|
+
{ from: 'embed', to: 'report', port: 'embed' },
|
|
38
|
+
{ from: 'novelty', to: 'report', port: 'novelty' },
|
|
39
|
+
{ from: 'contradiction', to: 'report', port: 'contradiction' },
|
|
40
|
+
{ from: 'crystallize', to: 'report', port: 'crystallize' },
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
/** The node ids, in pipeline order — what the UI renders as the status strip. */
|
|
44
|
+
export const PIPELINE_NODES = ['observations', 'embed', 'novelty', 'contradiction', 'crystallize', 'report'];
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @tangleai/pipeline barrel. */
|
|
2
|
+
export { PIPELINE_DAG, PIPELINE_NODES } from './dag.ts';
|
|
3
|
+
export { createPipeline, DEFAULT_THRESHOLDS } from './run.ts';
|
|
4
|
+
export type { Pipeline, PipelineOptions, PipelineReport, PipelineThresholds, DagNodeRecord, } from './run.ts';
|
|
5
|
+
export { dagToMermaid } from './mermaid.ts';
|
|
6
|
+
export { createOfflineEmbedder, OFFLINE_EMBEDDER_DIMS, numericContrastJudge } from './standins.ts';
|
|
7
|
+
export type { Judge } from './standins.ts';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** @tangleai/pipeline barrel. */
|
|
2
|
+
export { PIPELINE_DAG, PIPELINE_NODES } from "./dag.js";
|
|
3
|
+
export { createPipeline, DEFAULT_THRESHOLDS } from "./run.js";
|
|
4
|
+
export { dagToMermaid } from "./mermaid.js";
|
|
5
|
+
export { createOfflineEmbedder, OFFLINE_EMBEDDER_DIMS, numericContrastJudge } from "./standins.js";
|
package/src/mermaid.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pipeline document, projected to Mermaid text.
|
|
3
|
+
*
|
|
4
|
+
* Uses the suite's own projection chain — the `dag-to-flowchart` JSLT
|
|
5
|
+
* stylesheet shipped by @jarenjs/mermaid, `transformJson` from
|
|
6
|
+
* @jarenjs/json, `toMermaid` over a diagram document — so the drawing
|
|
7
|
+
* is DERIVED from the executable document, never hand-drawn. Node
|
|
8
|
+
* statuses are deliberately NOT painted into the diagram; the UI
|
|
9
|
+
* renders them as a strip beside it (data changes fast, topology
|
|
10
|
+
* doesn't).
|
|
11
|
+
*/
|
|
12
|
+
export declare function dagToMermaid(doc?: any): string;
|
package/src/mermaid.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pipeline document, projected to Mermaid text.
|
|
3
|
+
*
|
|
4
|
+
* Uses the suite's own projection chain — the `dag-to-flowchart` JSLT
|
|
5
|
+
* stylesheet shipped by @jarenjs/mermaid, `transformJson` from
|
|
6
|
+
* @jarenjs/json, `toMermaid` over a diagram document — so the drawing
|
|
7
|
+
* is DERIVED from the executable document, never hand-drawn. Node
|
|
8
|
+
* statuses are deliberately NOT painted into the diagram; the UI
|
|
9
|
+
* renders them as a strip beside it (data changes fast, topology
|
|
10
|
+
* doesn't).
|
|
11
|
+
*/
|
|
12
|
+
import { toMermaid, diagramDocument } from '@jarenjs/mermaid';
|
|
13
|
+
import { transformJson } from '@jarenjs/json/jslt';
|
|
14
|
+
import dagToFlowchart from '@jarenjs/mermaid/stylesheets/dag-to-flowchart.jslt.json' with { type: 'json' };
|
|
15
|
+
import { PIPELINE_DAG } from "./dag.js";
|
|
16
|
+
export function dagToMermaid(doc = PIPELINE_DAG) {
|
|
17
|
+
const ast = transformJson(dagToFlowchart, doc);
|
|
18
|
+
return toMermaid(diagramDocument('flowchart', {}, ast, { hash: '0', direction: 'TD', title: null }));
|
|
19
|
+
}
|
package/src/run.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiling and running the pipeline document.
|
|
3
|
+
*
|
|
4
|
+
* `createPipeline` binds the DAG's named task handlers to injected
|
|
5
|
+
* seams — the store (any MemoryStore: in-memory for tests and the pages
|
|
6
|
+
* demo, SQLite in the desktop app), an embedder (the @jarenjs/ai seam:
|
|
7
|
+
* `{ embed, model, dims }` — `createEmbeddingClient` for a wire,
|
|
8
|
+
* `createHashEmbedder` as the offline default), a contradiction judge,
|
|
9
|
+
* a clock, and the tuned thresholds (salvaged memflow defaults). The
|
|
10
|
+
* document stays pure JSON; everything replaceable arrives here.
|
|
11
|
+
*
|
|
12
|
+
* Every vector the embed node writes carries its identity
|
|
13
|
+
* (`embeddedBy: { model, dims }`), the jarenjs rule that lets every
|
|
14
|
+
* later policy refuse to compare vectors from two models.
|
|
15
|
+
*
|
|
16
|
+
* `run()` returns a compact report; the units themselves live in the
|
|
17
|
+
* store, not in the return value. Per-node timing/status records stream
|
|
18
|
+
* through `onNode` exactly as @jarenjs/flow emits them.
|
|
19
|
+
*/
|
|
20
|
+
import type { Embedder } from '@jarenjs/ai/embed';
|
|
21
|
+
import { type MemoryStore, type MemoryUnitInput } from '@tangleai/memory';
|
|
22
|
+
import { type Judge } from './standins.ts';
|
|
23
|
+
export interface PipelineThresholds {
|
|
24
|
+
/** Near-verbatim repeat filter; keep HIGH so contradictions survive. */
|
|
25
|
+
novelty?: number;
|
|
26
|
+
/** Similar-pair candidate threshold for the judge. */
|
|
27
|
+
contradiction?: number;
|
|
28
|
+
/** Paraphrase merge threshold. */
|
|
29
|
+
crystallize?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface PipelineOptions {
|
|
32
|
+
store: MemoryStore;
|
|
33
|
+
embedder?: Embedder;
|
|
34
|
+
judge?: Judge;
|
|
35
|
+
now?: () => string;
|
|
36
|
+
thresholds?: PipelineThresholds;
|
|
37
|
+
}
|
|
38
|
+
export interface DagNodeRecord {
|
|
39
|
+
id: string;
|
|
40
|
+
status: 'ok' | 'error' | 'aborted' | 'restored';
|
|
41
|
+
ms: number;
|
|
42
|
+
}
|
|
43
|
+
export interface PipelineReport {
|
|
44
|
+
observations: number;
|
|
45
|
+
embedded: number;
|
|
46
|
+
model: string;
|
|
47
|
+
novelty: {
|
|
48
|
+
admitted: number;
|
|
49
|
+
filtered: number;
|
|
50
|
+
};
|
|
51
|
+
/** Every attempt is judged or failed; every confirmed verdict is applied or skipped. */
|
|
52
|
+
contradiction: {
|
|
53
|
+
attempted: number;
|
|
54
|
+
judged: number;
|
|
55
|
+
judgeFailures: number;
|
|
56
|
+
confirmed: number;
|
|
57
|
+
contradictions: number;
|
|
58
|
+
applicationSkips: number;
|
|
59
|
+
resolutions: string[];
|
|
60
|
+
};
|
|
61
|
+
/** Every planned merge is written or skipped. */
|
|
62
|
+
crystallize: {
|
|
63
|
+
examined: number;
|
|
64
|
+
planned: number;
|
|
65
|
+
merged: number;
|
|
66
|
+
applicationSkips: number;
|
|
67
|
+
};
|
|
68
|
+
memories: {
|
|
69
|
+
live: number;
|
|
70
|
+
total: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export interface Pipeline {
|
|
74
|
+
run(observations: MemoryUnitInput[], options?: {
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
onNode?: (record: DagNodeRecord) => void;
|
|
77
|
+
}): Promise<PipelineReport>;
|
|
78
|
+
}
|
|
79
|
+
export declare const DEFAULT_THRESHOLDS: Required<PipelineThresholds>;
|
|
80
|
+
export declare function createPipeline(options: PipelineOptions): Pipeline;
|
package/src/run.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiling and running the pipeline document.
|
|
3
|
+
*
|
|
4
|
+
* `createPipeline` binds the DAG's named task handlers to injected
|
|
5
|
+
* seams — the store (any MemoryStore: in-memory for tests and the pages
|
|
6
|
+
* demo, SQLite in the desktop app), an embedder (the @jarenjs/ai seam:
|
|
7
|
+
* `{ embed, model, dims }` — `createEmbeddingClient` for a wire,
|
|
8
|
+
* `createHashEmbedder` as the offline default), a contradiction judge,
|
|
9
|
+
* a clock, and the tuned thresholds (salvaged memflow defaults). The
|
|
10
|
+
* document stays pure JSON; everything replaceable arrives here.
|
|
11
|
+
*
|
|
12
|
+
* Every vector the embed node writes carries its identity
|
|
13
|
+
* (`embeddedBy: { model, dims }`), the jarenjs rule that lets every
|
|
14
|
+
* later policy refuse to compare vectors from two models.
|
|
15
|
+
*
|
|
16
|
+
* `run()` returns a compact report; the units themselves live in the
|
|
17
|
+
* store, not in the return value. Per-node timing/status records stream
|
|
18
|
+
* through `onNode` exactly as @jarenjs/flow emits them.
|
|
19
|
+
*/
|
|
20
|
+
import { compileDag } from '@jarenjs/flow';
|
|
21
|
+
import { createMemoryUnit, noveltyGate, planContradictionPairs, resolveContradictions, planCrystallization, applyCrystallization, } from '@tangleai/memory';
|
|
22
|
+
import { createOfflineEmbedder, numericContrastJudge } from "./standins.js";
|
|
23
|
+
import { PIPELINE_DAG } from "./dag.js";
|
|
24
|
+
export const DEFAULT_THRESHOLDS = {
|
|
25
|
+
novelty: 0.97,
|
|
26
|
+
contradiction: 0.8,
|
|
27
|
+
crystallize: 0.9,
|
|
28
|
+
};
|
|
29
|
+
export function createPipeline(options) {
|
|
30
|
+
const store = options.store;
|
|
31
|
+
const embedder = options.embedder ?? createOfflineEmbedder();
|
|
32
|
+
const judge = options.judge ?? numericContrastJudge();
|
|
33
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
34
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds };
|
|
35
|
+
const tasks = {
|
|
36
|
+
async embed({ input }, signal) {
|
|
37
|
+
const observations = input;
|
|
38
|
+
const missing = observations.filter((o) => o.embedding === undefined);
|
|
39
|
+
const vectors = missing.length > 0
|
|
40
|
+
? await embedder.embed(missing.map((o) => o.text), { signal })
|
|
41
|
+
: [];
|
|
42
|
+
// the identity: the embedder's settled width, or the reply's
|
|
43
|
+
const dims = embedder.dims ?? vectors[0]?.length;
|
|
44
|
+
const embeddedBy = dims === undefined ? undefined : { model: embedder.model, dims };
|
|
45
|
+
let next = 0;
|
|
46
|
+
const units = observations.map((o) => {
|
|
47
|
+
if (o.embedding !== undefined)
|
|
48
|
+
return createMemoryUnit({ ...o, confidence: o.confidence ?? 0.5 });
|
|
49
|
+
// the seam answers Float32Array; the record stores plain numbers
|
|
50
|
+
const vector = Array.from(vectors[next++]);
|
|
51
|
+
return createMemoryUnit({ ...o, embedding: vector, embeddedBy, confidence: o.confidence ?? 0.5 });
|
|
52
|
+
});
|
|
53
|
+
return { units, embedded: missing.length, model: embedder.model };
|
|
54
|
+
},
|
|
55
|
+
async novelty({ input }) {
|
|
56
|
+
const units = input.units;
|
|
57
|
+
const existing = await store.list();
|
|
58
|
+
const { novel, filtered } = noveltyGate(units, existing, { threshold: thresholds.novelty });
|
|
59
|
+
for (const unit of novel)
|
|
60
|
+
await store.put(unit);
|
|
61
|
+
return { admitted: novel.length, filtered: filtered.length };
|
|
62
|
+
},
|
|
63
|
+
async contradiction() {
|
|
64
|
+
const units = await store.list();
|
|
65
|
+
const pairs = planContradictionPairs(units, { threshold: thresholds.contradiction });
|
|
66
|
+
const outcome = await resolveContradictions(store, pairs, { now, judge });
|
|
67
|
+
return {
|
|
68
|
+
attempted: outcome.attempted,
|
|
69
|
+
judged: outcome.judged,
|
|
70
|
+
judgeFailures: outcome.judgeFailures,
|
|
71
|
+
confirmed: outcome.confirmed,
|
|
72
|
+
contradictions: outcome.contradictions,
|
|
73
|
+
applicationSkips: outcome.applicationSkips,
|
|
74
|
+
resolutions: outcome.resolutions.map((r) => r.text),
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
async crystallize() {
|
|
78
|
+
const plan = planCrystallization(await store.list(), { threshold: thresholds.crystallize });
|
|
79
|
+
const outcome = await applyCrystallization(store, plan, { now });
|
|
80
|
+
return {
|
|
81
|
+
examined: plan.examined,
|
|
82
|
+
planned: outcome.planned,
|
|
83
|
+
merged: outcome.crystallized,
|
|
84
|
+
applicationSkips: outcome.applicationSkips,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const dag = compileDag(PIPELINE_DAG, { tasks });
|
|
89
|
+
return {
|
|
90
|
+
async run(observations, runOptions = {}) {
|
|
91
|
+
const result = await dag.run(observations, {
|
|
92
|
+
signal: runOptions.signal,
|
|
93
|
+
onNode: runOptions.onNode,
|
|
94
|
+
});
|
|
95
|
+
const all = await store.list();
|
|
96
|
+
const live = all.filter((u) => u.supersededBy === undefined);
|
|
97
|
+
return {
|
|
98
|
+
observations: observations.length,
|
|
99
|
+
embedded: result.embed.embedded,
|
|
100
|
+
model: result.embed.model,
|
|
101
|
+
novelty: result.novelty,
|
|
102
|
+
contradiction: result.contradiction,
|
|
103
|
+
crystallize: result.crystallize,
|
|
104
|
+
memories: { live: live.length, total: all.length },
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two offline stand-ins — an embedder and a contradiction judge —
|
|
3
|
+
* that let the whole pipeline run with zero network, promoted out of
|
|
4
|
+
* `examples/skeleton.ts` because the desktop app and the pages demo
|
|
5
|
+
* need them as real seams.
|
|
6
|
+
*
|
|
7
|
+
* `createOfflineEmbedder` is @jarenjs/ai's `createHashEmbedder` — the
|
|
8
|
+
* suite's own deterministic hashed-trigram reference, behind the same
|
|
9
|
+
* `{ embed, model, dims }` seam a wire client fills — at the width the
|
|
10
|
+
* pipeline's thresholds were measured against. Measured over the
|
|
11
|
+
* skeleton corpus (2026-08-26, `hash-trigram-<dims>`, cosine), against
|
|
12
|
+
* the defaults novelty 0.97 / crystallize 0.9 / contradiction 0.8:
|
|
13
|
+
*
|
|
14
|
+
* dims repeat paraphrase contradiction max-unrelated
|
|
15
|
+
* 64 0.989 0.931 0.964 0.606 ← every pair on its side of every threshold
|
|
16
|
+
* 128 0.985 0.902 0.954 0.424 ← paraphrase a hair over the 0.9 merge line
|
|
17
|
+
* 256 0.984 0.888 0.941 0.323 ← paraphrase under the merge line: never crystallized
|
|
18
|
+
* 512 0.983 0.884 0.936 0.225
|
|
19
|
+
*
|
|
20
|
+
* 64 — the suite's own default — is the width with margin on all four:
|
|
21
|
+
* wider buckets spread the shared trigrams thinner, so the paraphrase
|
|
22
|
+
* loses its merge long before an unrelated pair threatens the 0.8
|
|
23
|
+
* judge line. It stays demo-grade and lexical — a real model behind
|
|
24
|
+
* the same seam is the upgrade.
|
|
25
|
+
*
|
|
26
|
+
* `numericContrastJudge` flags two records that agree in words but
|
|
27
|
+
* disagree in figures — the rule stand-in for an LLM contradiction
|
|
28
|
+
* judge (`createStructuredOutput` over `contradictionMessages`).
|
|
29
|
+
*/
|
|
30
|
+
import { type Embedder } from '@jarenjs/ai/embed';
|
|
31
|
+
import type { MemoryUnit } from '@tangleai/core/schemas/memory';
|
|
32
|
+
import type { ContradictionVerdict } from '@tangleai/memory';
|
|
33
|
+
/** The measured width (see the header). */
|
|
34
|
+
export declare const OFFLINE_EMBEDDER_DIMS = 64;
|
|
35
|
+
/** The offline embedder: the suite's hash reference at the measured width. */
|
|
36
|
+
export declare function createOfflineEmbedder(): Embedder & {
|
|
37
|
+
dims: number;
|
|
38
|
+
};
|
|
39
|
+
export type Judge = (a: MemoryUnit, b: MemoryUnit) => Promise<ContradictionVerdict>;
|
|
40
|
+
/** Same subject, different figures → contradiction; newer text wins. */
|
|
41
|
+
export declare function numericContrastJudge(): Judge;
|
package/src/standins.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two offline stand-ins — an embedder and a contradiction judge —
|
|
3
|
+
* that let the whole pipeline run with zero network, promoted out of
|
|
4
|
+
* `examples/skeleton.ts` because the desktop app and the pages demo
|
|
5
|
+
* need them as real seams.
|
|
6
|
+
*
|
|
7
|
+
* `createOfflineEmbedder` is @jarenjs/ai's `createHashEmbedder` — the
|
|
8
|
+
* suite's own deterministic hashed-trigram reference, behind the same
|
|
9
|
+
* `{ embed, model, dims }` seam a wire client fills — at the width the
|
|
10
|
+
* pipeline's thresholds were measured against. Measured over the
|
|
11
|
+
* skeleton corpus (2026-08-26, `hash-trigram-<dims>`, cosine), against
|
|
12
|
+
* the defaults novelty 0.97 / crystallize 0.9 / contradiction 0.8:
|
|
13
|
+
*
|
|
14
|
+
* dims repeat paraphrase contradiction max-unrelated
|
|
15
|
+
* 64 0.989 0.931 0.964 0.606 ← every pair on its side of every threshold
|
|
16
|
+
* 128 0.985 0.902 0.954 0.424 ← paraphrase a hair over the 0.9 merge line
|
|
17
|
+
* 256 0.984 0.888 0.941 0.323 ← paraphrase under the merge line: never crystallized
|
|
18
|
+
* 512 0.983 0.884 0.936 0.225
|
|
19
|
+
*
|
|
20
|
+
* 64 — the suite's own default — is the width with margin on all four:
|
|
21
|
+
* wider buckets spread the shared trigrams thinner, so the paraphrase
|
|
22
|
+
* loses its merge long before an unrelated pair threatens the 0.8
|
|
23
|
+
* judge line. It stays demo-grade and lexical — a real model behind
|
|
24
|
+
* the same seam is the upgrade.
|
|
25
|
+
*
|
|
26
|
+
* `numericContrastJudge` flags two records that agree in words but
|
|
27
|
+
* disagree in figures — the rule stand-in for an LLM contradiction
|
|
28
|
+
* judge (`createStructuredOutput` over `contradictionMessages`).
|
|
29
|
+
*/
|
|
30
|
+
import { createHashEmbedder } from '@jarenjs/ai/embed';
|
|
31
|
+
/** The measured width (see the header). */
|
|
32
|
+
export const OFFLINE_EMBEDDER_DIMS = 64;
|
|
33
|
+
/** The offline embedder: the suite's hash reference at the measured width. */
|
|
34
|
+
export function createOfflineEmbedder() {
|
|
35
|
+
return createHashEmbedder({ dims: OFFLINE_EMBEDDER_DIMS });
|
|
36
|
+
}
|
|
37
|
+
/** Same subject, different figures → contradiction; newer text wins. */
|
|
38
|
+
export function numericContrastJudge() {
|
|
39
|
+
const numbers = (t) => t.match(/\d+(?:\.\d+)?/g)?.join(',') ?? '';
|
|
40
|
+
return async (a, b) => {
|
|
41
|
+
if (numbers(a.text) !== numbers(b.text)) {
|
|
42
|
+
const newer = a.at <= b.at ? b : a;
|
|
43
|
+
return { contradiction: true, reason: 'same subject, different figures', resolution: newer.text };
|
|
44
|
+
}
|
|
45
|
+
return { contradiction: false };
|
|
46
|
+
};
|
|
47
|
+
}
|