@really-knows-ai/foundry 2.3.1 → 3.0.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/README.md +200 -198
- package/dist/.opencode/plugins/foundry-tools/appraiser-tools.js +28 -0
- package/dist/.opencode/plugins/foundry-tools/artefact-tools.js +58 -0
- package/dist/.opencode/plugins/foundry-tools/assay-tools.js +92 -0
- package/dist/.opencode/plugins/foundry-tools/attestation-tools.js +191 -0
- package/dist/.opencode/plugins/foundry-tools/config-create-tools.js +128 -0
- package/dist/.opencode/plugins/foundry-tools/config-law-tools.js +380 -0
- package/dist/.opencode/plugins/foundry-tools/config-tools.js +43 -0
- package/dist/.opencode/plugins/foundry-tools/feedback-tools.js +234 -0
- package/dist/.opencode/plugins/foundry-tools/git-helpers.js +354 -0
- package/dist/.opencode/plugins/foundry-tools/git-tools.js +181 -0
- package/dist/.opencode/plugins/foundry-tools/helpers.js +340 -0
- package/dist/.opencode/plugins/foundry-tools/history-tools.js +20 -0
- package/dist/.opencode/plugins/foundry-tools/memory-admin-tools.js +296 -0
- package/dist/.opencode/plugins/foundry-tools/memory-helpers.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/memory-tools.js +286 -0
- package/dist/.opencode/plugins/foundry-tools/orchestrate-tool.js +159 -0
- package/dist/.opencode/plugins/foundry-tools/snapshot-tools.js +104 -0
- package/dist/.opencode/plugins/foundry-tools/stage-tools.js +186 -0
- package/dist/.opencode/plugins/foundry-tools/validate-tools.js +263 -0
- package/dist/.opencode/plugins/foundry-tools/workfile-tools.js +102 -0
- package/dist/.opencode/plugins/foundry.js +105 -0
- package/dist/CHANGELOG.md +490 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +278 -0
- package/dist/docs/README.md +59 -0
- package/dist/docs/architecture.md +434 -0
- package/dist/docs/concepts.md +396 -0
- package/dist/docs/getting-started.md +345 -0
- package/dist/docs/memory-maintenance.md +176 -0
- package/dist/docs/tools.md +1411 -0
- package/dist/docs/work-spec.md +283 -0
- package/dist/scripts/lib/artefacts.js +151 -0
- package/dist/scripts/lib/assay/loader.js +151 -0
- package/dist/scripts/lib/assay/parse-jsonl.js +102 -0
- package/dist/scripts/lib/assay/permissions.js +52 -0
- package/dist/scripts/lib/assay/run.js +219 -0
- package/dist/scripts/lib/assay/spawn-with-timeout.js +138 -0
- package/dist/scripts/lib/attestation/attest.js +111 -0
- package/dist/scripts/lib/attestation/canonical-json.js +109 -0
- package/dist/scripts/lib/attestation/hash.js +17 -0
- package/dist/scripts/lib/attestation/parse.js +14 -0
- package/dist/scripts/lib/attestation/payload.js +106 -0
- package/dist/scripts/lib/attestation/render.js +16 -0
- package/dist/scripts/lib/attestation/verify.js +15 -0
- package/dist/scripts/lib/branch-guard.js +72 -0
- package/dist/scripts/lib/config-creators/appraiser.js +9 -0
- package/dist/scripts/lib/config-creators/artefact-type.js +9 -0
- package/dist/scripts/lib/config-creators/cycle.js +11 -0
- package/dist/scripts/lib/config-creators/factory.js +49 -0
- package/dist/scripts/lib/config-creators/flow.js +11 -0
- package/dist/scripts/lib/config-validators/appraiser.js +49 -0
- package/dist/scripts/lib/config-validators/artefact-type.js +38 -0
- package/dist/scripts/lib/config-validators/cycle.js +131 -0
- package/dist/scripts/lib/config-validators/flow.js +57 -0
- package/dist/scripts/lib/config-validators/helpers.js +96 -0
- package/dist/scripts/lib/config-validators/law.js +96 -0
- package/dist/scripts/lib/config.js +393 -0
- package/dist/scripts/lib/failed-flow.js +131 -0
- package/dist/scripts/lib/feedback-store.js +249 -0
- package/dist/scripts/lib/feedback-transitions.js +105 -0
- package/dist/scripts/lib/finalize.js +70 -0
- package/dist/scripts/lib/foundational-guards.js +13 -0
- package/dist/scripts/lib/git-bridge.js +77 -0
- package/dist/scripts/lib/git-finish/work-finish.js +233 -0
- package/dist/scripts/lib/git-policy.js +101 -0
- package/dist/scripts/lib/guards.js +125 -0
- package/dist/scripts/lib/history.js +132 -0
- package/dist/scripts/lib/memory/admin/create-edge-type.js +91 -0
- package/dist/scripts/lib/memory/admin/create-entity-type.js +43 -0
- package/dist/scripts/lib/memory/admin/create-extractor.js +67 -0
- package/dist/scripts/lib/memory/admin/drop-edge-type.js +40 -0
- package/dist/scripts/lib/memory/admin/drop-entity-type.js +172 -0
- package/dist/scripts/lib/memory/admin/dump.js +47 -0
- package/dist/scripts/lib/memory/admin/helpers.js +31 -0
- package/dist/scripts/lib/memory/admin/init.js +170 -0
- package/dist/scripts/lib/memory/admin/live-store.js +76 -0
- package/dist/scripts/lib/memory/admin/reembed.js +285 -0
- package/dist/scripts/lib/memory/admin/rename-edge-type.js +54 -0
- package/dist/scripts/lib/memory/admin/rename-entity-type.js +151 -0
- package/dist/scripts/lib/memory/admin/reset.js +24 -0
- package/dist/scripts/lib/memory/admin/vacuum.js +9 -0
- package/dist/scripts/lib/memory/admin/validate.js +19 -0
- package/dist/scripts/lib/memory/config.js +149 -0
- package/dist/scripts/lib/memory/cozo.js +136 -0
- package/dist/scripts/lib/memory/drift.js +71 -0
- package/dist/scripts/lib/memory/embeddings.js +128 -0
- package/dist/scripts/lib/memory/frontmatter.js +75 -0
- package/dist/scripts/lib/memory/ndjson.js +84 -0
- package/dist/scripts/lib/memory/paths.js +25 -0
- package/dist/scripts/lib/memory/permissions.js +41 -0
- package/dist/scripts/lib/memory/prompt.js +109 -0
- package/dist/scripts/lib/memory/query.js +56 -0
- package/dist/scripts/lib/memory/reads.js +109 -0
- package/dist/scripts/lib/memory/schema.js +64 -0
- package/dist/scripts/lib/memory/search.js +73 -0
- package/dist/scripts/lib/memory/singleton.js +49 -0
- package/dist/scripts/lib/memory/store.js +162 -0
- package/dist/scripts/lib/memory/types.js +93 -0
- package/dist/scripts/lib/memory/validate.js +58 -0
- package/dist/scripts/lib/memory/writes.js +40 -0
- package/{scripts → dist/scripts}/lib/pending.js +7 -2
- package/dist/scripts/lib/secret.js +59 -0
- package/{scripts → dist/scripts}/lib/slug.js +3 -2
- package/dist/scripts/lib/snapshot/finish.js +103 -0
- package/dist/scripts/lib/snapshot/inspect.js +253 -0
- package/dist/scripts/lib/snapshot/render.js +55 -0
- package/dist/scripts/lib/sort-fs-check.js +121 -0
- package/dist/scripts/lib/sort-routing.js +101 -0
- package/{scripts → dist/scripts}/lib/stage-guard.js +12 -6
- package/{scripts → dist/scripts}/lib/state.js +4 -0
- package/dist/scripts/lib/token.js +57 -0
- package/dist/scripts/lib/tracing.js +59 -0
- package/dist/scripts/lib/ulid.js +100 -0
- package/dist/scripts/lib/validator-jsonl.js +162 -0
- package/{scripts → dist/scripts}/lib/workfile.js +38 -20
- package/dist/scripts/orchestrate-cycle.js +215 -0
- package/dist/scripts/orchestrate-phases.js +314 -0
- package/dist/scripts/orchestrate.js +163 -0
- package/dist/scripts/sort.js +278 -0
- package/{skills → dist/skills}/add-appraiser/SKILL.md +42 -6
- package/{skills → dist/skills}/add-artefact-type/SKILL.md +49 -21
- package/{skills → dist/skills}/add-cycle/SKILL.md +60 -14
- package/dist/skills/add-extractor/SKILL.md +133 -0
- package/{skills → dist/skills}/add-flow/SKILL.md +39 -7
- package/dist/skills/add-law/SKILL.md +191 -0
- package/dist/skills/add-memory-edge-type/SKILL.md +52 -0
- package/dist/skills/add-memory-entity-type/SKILL.md +74 -0
- package/{skills → dist/skills}/appraise/SKILL.md +62 -13
- package/dist/skills/assay/SKILL.md +72 -0
- package/dist/skills/change-embedding-model/SKILL.md +58 -0
- package/dist/skills/drop-memory-edge-type/SKILL.md +54 -0
- package/dist/skills/drop-memory-entity-type/SKILL.md +57 -0
- package/dist/skills/dry-run/SKILL.md +116 -0
- package/{skills → dist/skills}/flow/SKILL.md +15 -2
- package/dist/skills/forge/SKILL.md +121 -0
- package/dist/skills/human-appraise/SKILL.md +153 -0
- package/{skills → dist/skills}/init-foundry/SKILL.md +23 -4
- package/dist/skills/init-memory/SKILL.md +92 -0
- package/{skills → dist/skills}/orchestrate/SKILL.md +30 -4
- package/dist/skills/quench/SKILL.md +99 -0
- package/{skills → dist/skills}/refresh-agents/SKILL.md +1 -1
- package/dist/skills/rename-memory-edge-type/SKILL.md +50 -0
- package/dist/skills/rename-memory-entity-type/SKILL.md +51 -0
- package/dist/skills/reset-memory/SKILL.md +54 -0
- package/dist/skills/upgrade-foundry/SKILL.md +192 -0
- package/package.json +34 -17
- package/.opencode/plugins/foundry.js +0 -761
- package/CHANGELOG.md +0 -90
- package/docs/concepts.md +0 -59
- package/docs/getting-started.md +0 -78
- package/docs/work-spec.md +0 -193
- package/scripts/lib/artefacts.js +0 -124
- package/scripts/lib/config.js +0 -175
- package/scripts/lib/feedback-transitions.js +0 -25
- package/scripts/lib/feedback.js +0 -440
- package/scripts/lib/finalize.js +0 -41
- package/scripts/lib/history.js +0 -59
- package/scripts/lib/secret.js +0 -23
- package/scripts/lib/tags.js +0 -108
- package/scripts/lib/token.js +0 -26
- package/scripts/orchestrate.js +0 -418
- package/scripts/sort.js +0 -370
- package/scripts/validate-tags.js +0 -54
- package/skills/add-law/SKILL.md +0 -105
- package/skills/forge/SKILL.md +0 -88
- package/skills/human-appraise/SKILL.md +0 -82
- package/skills/quench/SKILL.md +0 -62
- package/skills/upgrade-foundry/SKILL.md +0 -216
- /package/{skills → dist/skills}/list-agents/SKILL.md +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
let CozoDb = null;
|
|
5
|
+
try {
|
|
6
|
+
({ CozoDb } = require('cozo-node'));
|
|
7
|
+
} catch {
|
|
8
|
+
// cozo-node is optional; memory features are unavailable on this platform
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertCozo() {
|
|
12
|
+
if (!CozoDb) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'cozo-node is not installed on this platform. ' +
|
|
15
|
+
'Install it to enable the memory subsystem: pnpm add cozo-node'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function openMemoryDb(dbPath) {
|
|
21
|
+
assertCozo();
|
|
22
|
+
return new CozoDb('sqlite', dbPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function closeMemoryDb(db) {
|
|
26
|
+
if (db && typeof db.close === 'function') db.close();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Canonical Cozo string literal encoder.
|
|
31
|
+
*
|
|
32
|
+
* Emits a **single-quoted** literal. Single-quoted literals honour standard
|
|
33
|
+
* escape sequences and safely handle embedded `"`, `\`, newlines, CR, tabs,
|
|
34
|
+
* and backslashes, producing a safe round-trip for all supported characters.
|
|
35
|
+
* (Cozo's `"..."` form is raw and stores backslash sequences literally.)
|
|
36
|
+
*
|
|
37
|
+
* Used by every query builder in this package so read and write paths never
|
|
38
|
+
* diverge on what they consider a safe literal. NUL characters are rejected
|
|
39
|
+
* here because Cozo single-quoted string literals lack `\0` escape support.
|
|
40
|
+
* The write path (putEntity, relate, unrelate) also validates for NUL in
|
|
41
|
+
* validateEntityWrite/validateEdgeWrite, providing defence in depth for
|
|
42
|
+
* import paths and future callers.
|
|
43
|
+
*/
|
|
44
|
+
export function cozoStringLit(s) {
|
|
45
|
+
const str = String(s);
|
|
46
|
+
if (str.includes('\0')) {
|
|
47
|
+
throw new Error('cozoStringLit: string must not contain NUL (\\0)');
|
|
48
|
+
}
|
|
49
|
+
const escaped = str
|
|
50
|
+
.replace(/\\/g, '\\\\')
|
|
51
|
+
.replace(/'/g, "\\'")
|
|
52
|
+
.replace(/\n/g, '\\n')
|
|
53
|
+
.replace(/\r/g, '\\r')
|
|
54
|
+
.replace(/\t/g, '\\t');
|
|
55
|
+
return `'${escaped}'`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function entRelName(type) { return `ent_${type}`; }
|
|
59
|
+
function edgeRelName(type) { return `edge_${type}`; }
|
|
60
|
+
|
|
61
|
+
async function relationExists(db, name) {
|
|
62
|
+
const res = await db.run('::relations');
|
|
63
|
+
return res.rows.some((r) => r[0] === name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function listRelations(db) {
|
|
67
|
+
const res = await db.run('::relations');
|
|
68
|
+
return res.rows.map((r) => r[0]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createEntityRelation(db, type, opts = {}) {
|
|
72
|
+
const name = entRelName(type);
|
|
73
|
+
if (await relationExists(db, name)) return;
|
|
74
|
+
const { dim } = opts;
|
|
75
|
+
// Cozo 0.7 requires a typed vector (<F32; N>) for HNSW indexing. When no
|
|
76
|
+
// embeddings are configured we keep the flexible [Float]? column so the
|
|
77
|
+
// relation can still be created and written to without embeddings.
|
|
78
|
+
const embeddingCol = Number.isInteger(dim) && dim > 0
|
|
79
|
+
? `embedding: <F32; ${dim}>? default null`
|
|
80
|
+
: `embedding: [Float]? default null`;
|
|
81
|
+
await db.run(`:create ${name} { name: String => value: String, ${embeddingCol} }`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function createEdgeRelation(db, type) {
|
|
85
|
+
const name = edgeRelName(type);
|
|
86
|
+
if (await relationExists(db, name)) return;
|
|
87
|
+
await db.run(`:create ${name} { from_type: String, from_name: String, to_type: String, to_name: String }`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function dropRelation(db, relationName) {
|
|
91
|
+
await db.run(`::remove ${relationName}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isIgnorableError(err, patterns) {
|
|
95
|
+
const msg = String(err && (err.display || err.message || err));
|
|
96
|
+
return patterns.some((p) => p.test(msg));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assertHnswDim(dim) {
|
|
100
|
+
if (!Number.isInteger(dim) || dim <= 0) throw new Error('createHnswIndex: dim must be positive integer');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveHnswOpts(opts) {
|
|
104
|
+
const { dim, ef = 50, m = 16 } = opts ?? {};
|
|
105
|
+
return { dim, ef, m };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function createHnswIndex(db, relationName, opts) {
|
|
109
|
+
const { dim, ef, m } = resolveHnswOpts(opts);
|
|
110
|
+
assertHnswDim(dim);
|
|
111
|
+
try {
|
|
112
|
+
await db.run(`::hnsw create ${relationName}:vec { fields: [embedding], dim: ${dim}, ef: ${ef}, m: ${m} }`);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (isIgnorableError(err, [/already exists/i, /already created/i])) return;
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function dropHnswIndex(db, relationName) {
|
|
120
|
+
try {
|
|
121
|
+
await db.run(`::hnsw drop ${relationName}:vec`);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (isIgnorableError(err, [/not found/i, /does not exist/i, /no such/i])) return;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function checkpoint(db) {
|
|
129
|
+
try {
|
|
130
|
+
await db.run('::compact');
|
|
131
|
+
} catch (err) {
|
|
132
|
+
if (!isIgnorableError(err, [/unknown system op/i, /parser::pest/i])) throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { entRelName, edgeRelName };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { hashFrontmatter } from './schema.js';
|
|
2
|
+
|
|
3
|
+
function compareLoadedType({ family, name, loadedEntry, recordedEntry }) {
|
|
4
|
+
const currentHash = hashFrontmatter(loadedEntry.frontmatter);
|
|
5
|
+
if (currentHash !== recordedEntry.frontmatterHash) {
|
|
6
|
+
return {
|
|
7
|
+
kind: 'frontmatter-mismatch',
|
|
8
|
+
typeFamily: family,
|
|
9
|
+
typeName: name,
|
|
10
|
+
message: `${family} type '${name}' frontmatter was modified outside of a skill`,
|
|
11
|
+
suggestedSkills: [`rename-memory-${family}-type`, `drop-memory-${family}-type`],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findMissingFiles({ family, loadedNames, recordedNames }) {
|
|
18
|
+
const items = [];
|
|
19
|
+
for (const name of recordedNames) {
|
|
20
|
+
if (!loadedNames.has(name)) {
|
|
21
|
+
items.push({
|
|
22
|
+
kind: 'missing-file',
|
|
23
|
+
typeFamily: family,
|
|
24
|
+
typeName: name,
|
|
25
|
+
message: `${family} type '${name}' is recorded in schema.json but its file is missing on disk`,
|
|
26
|
+
suggestedSkills: [`drop-memory-${family}-type`, `rename-memory-${family}-type`],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return items;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compareFamily({ family, loaded, recorded }) {
|
|
34
|
+
const items = [];
|
|
35
|
+
const loadedNames = new Set(Object.keys(loaded));
|
|
36
|
+
const recordedNames = new Set(Object.keys(recorded));
|
|
37
|
+
|
|
38
|
+
for (const name of loadedNames) {
|
|
39
|
+
if (!recordedNames.has(name)) {
|
|
40
|
+
items.push({
|
|
41
|
+
kind: 'unknown-type',
|
|
42
|
+
typeFamily: family,
|
|
43
|
+
typeName: name,
|
|
44
|
+
message: `${family} type '${name}' exists on disk but is not recorded in schema.json`,
|
|
45
|
+
suggestedSkills: [`add-memory-${family}-type`],
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const item = compareLoadedType({
|
|
50
|
+
family,
|
|
51
|
+
name,
|
|
52
|
+
loadedEntry: loaded[name],
|
|
53
|
+
recordedEntry: recorded[name],
|
|
54
|
+
});
|
|
55
|
+
if (item) {
|
|
56
|
+
items.push(item);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
items.push(...findMissingFiles({ family, loadedNames, recordedNames }));
|
|
61
|
+
|
|
62
|
+
return items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function detectDrift({ vocabulary, schema }) {
|
|
66
|
+
const items = [
|
|
67
|
+
...compareFamily({ family: 'entity', loaded: vocabulary.entities, recorded: schema.entities }),
|
|
68
|
+
...compareFamily({ family: 'edge', loaded: vocabulary.edges, recorded: schema.edges }),
|
|
69
|
+
];
|
|
70
|
+
return { hasDrift: items.length > 0, items };
|
|
71
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
|
|
2
|
+
|
|
3
|
+
function isRetryableStatus(statusCode) {
|
|
4
|
+
return statusCode !== null && statusCode !== undefined && RETRYABLE_STATUS_CODES.has(statusCode);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isRetryableError(error, statusCode) {
|
|
8
|
+
if (isRetryableStatus(statusCode)) return true;
|
|
9
|
+
if (error?.message?.match(/abort|timeout/i)) return true;
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertResponseOk(res) {
|
|
14
|
+
if (!res.ok) return res;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function parseEmbeddings(body) {
|
|
19
|
+
if (!Array.isArray(body.data)) throw new Error('embeddings provider returned malformed response (no data[])');
|
|
20
|
+
for (const item of body.data) {
|
|
21
|
+
if (!('index' in item)) {
|
|
22
|
+
throw new Error('embedding provider returned data without index field - cannot verify ordering');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return body.data
|
|
26
|
+
.sort((a, b) => a.index - b.index)
|
|
27
|
+
.map((d) => d.embedding);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function callOnce({ config, inputs }) {
|
|
31
|
+
const ac = new AbortController();
|
|
32
|
+
const t = setTimeout(() => ac.abort(), config.timeoutMs);
|
|
33
|
+
try {
|
|
34
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
35
|
+
if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
|
|
36
|
+
const res = await fetch(`${config.baseURL}/embeddings`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers,
|
|
39
|
+
body: JSON.stringify({ model: config.model, input: inputs }),
|
|
40
|
+
signal: ac.signal,
|
|
41
|
+
});
|
|
42
|
+
const errRes = assertResponseOk(res);
|
|
43
|
+
if (errRes) {
|
|
44
|
+
const text = await res.text().catch(() => '');
|
|
45
|
+
const error = new Error(`embeddings provider returned ${res.status}: ${text.slice(0, 500)}`);
|
|
46
|
+
error.statusCode = res.status;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const body = await res.json();
|
|
50
|
+
return parseEmbeddings(body);
|
|
51
|
+
} finally {
|
|
52
|
+
clearTimeout(t);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shouldGiveUp(attempt, maxAttempts, error) {
|
|
57
|
+
const isLastAttempt = attempt === maxAttempts - 1;
|
|
58
|
+
if (isLastAttempt) return true;
|
|
59
|
+
if (!isRetryableError(error, error.statusCode)) return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildRetryError(error, attempt) {
|
|
64
|
+
return new Error(`${error.message} (after ${attempt} retries)`, { cause: error });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function callWithRetry({ config, inputs }) {
|
|
68
|
+
const maxAttempts = 3;
|
|
69
|
+
const delays = [1000, 2000]; // 1s, 2s backoff
|
|
70
|
+
let lastError;
|
|
71
|
+
|
|
72
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
73
|
+
try {
|
|
74
|
+
return await callOnce({ config, inputs });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
lastError = error;
|
|
77
|
+
if (shouldGiveUp(attempt, maxAttempts, error)) {
|
|
78
|
+
if (attempt > 0) throw buildRetryError(error, attempt);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
await new Promise((resolve) => { setTimeout(resolve, delays[attempt]); });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// This should never be reached, but TypeScript/linters like it
|
|
86
|
+
throw lastError;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function assertVectorShape(v, dimensions) {
|
|
90
|
+
if (!Array.isArray(v) || v.length !== dimensions) {
|
|
91
|
+
throw new Error(`embedding dimension mismatch: expected ${dimensions}, got ${Array.isArray(v) ? v.length : 'non-array'}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertVectorFinite(v) {
|
|
96
|
+
for (const x of v) if (!Number.isFinite(x)) throw new Error('embedding contains non-finite number');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateVector(v, dimensions) {
|
|
100
|
+
assertVectorShape(v, dimensions);
|
|
101
|
+
assertVectorFinite(v);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function embed({ config, inputs }) {
|
|
105
|
+
if (!config.enabled) throw new Error('embeddings are disabled in memory config');
|
|
106
|
+
if (!Array.isArray(inputs) || inputs.length === 0) return [];
|
|
107
|
+
return embedBatches(config, inputs);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function embedBatches(config, inputs) {
|
|
111
|
+
const out = [];
|
|
112
|
+
for (let i = 0; i < inputs.length; i += config.batchSize) {
|
|
113
|
+
const batch = inputs.slice(i, i + config.batchSize);
|
|
114
|
+
const vectors = await callWithRetry({ config, inputs: batch });
|
|
115
|
+
for (const v of vectors) validateVector(v, config.dimensions);
|
|
116
|
+
out.push(...vectors);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function probeEmbeddings({ config }) {
|
|
122
|
+
try {
|
|
123
|
+
const out = await embed({ config, inputs: ['probe'] });
|
|
124
|
+
return { ok: true, dimensions: out[0].length };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return { ok: false, error: err.message ?? String(err) };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely parse a YAML string, rethrowing with a filename-prefixed message
|
|
5
|
+
* on failure so errors are actionable.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} yamlStr
|
|
8
|
+
* @param {string} filename
|
|
9
|
+
* @returns {unknown}
|
|
10
|
+
*/
|
|
11
|
+
function safeYamlLoad(yamlStr, filename) {
|
|
12
|
+
try {
|
|
13
|
+
return yaml.load(yamlStr);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
const msg = err?.message ?? String(err);
|
|
16
|
+
throw new Error(`${filename}: malformed YAML frontmatter: ${msg}`, { cause: err });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalise a parsed YAML value into a frontmatter object.
|
|
22
|
+
*
|
|
23
|
+
* @param {unknown} parsed
|
|
24
|
+
* @returns {object}
|
|
25
|
+
*/
|
|
26
|
+
function normaliseFrontmatter(parsed) {
|
|
27
|
+
if (parsed && typeof parsed === 'object') {
|
|
28
|
+
return /** @type {object} */ (parsed);
|
|
29
|
+
}
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a markdown document with YAML frontmatter.
|
|
35
|
+
*
|
|
36
|
+
* Accepts both LF and CRLF line endings (so files saved on Windows parse the
|
|
37
|
+
* same as files saved on Unix). Returns the parsed frontmatter object, the
|
|
38
|
+
* body (text after the closing `---`, untrimmed), and a `hasFrontmatter`
|
|
39
|
+
* flag. The body preserves original line endings.
|
|
40
|
+
*
|
|
41
|
+
* Throws with a filename-prefixed message on malformed YAML so errors are
|
|
42
|
+
* actionable (bare `YAMLException` from `js-yaml` gives no file context).
|
|
43
|
+
*
|
|
44
|
+
* NOTE: Intentionally duplicates core logic from ../workfile.js for different
|
|
45
|
+
* use cases. This version provides full error handling and returns structured
|
|
46
|
+
* metadata; workfile.js provides a simpler interface for WORK.md manipulation.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} text
|
|
49
|
+
* @param {{ filename?: string }} [opts]
|
|
50
|
+
* @returns {{ frontmatter: object, body: string, hasFrontmatter: boolean }}
|
|
51
|
+
*/
|
|
52
|
+
export function parseFrontmatter(text, { filename = '<unknown>' } = {}) {
|
|
53
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
54
|
+
if (!m) {
|
|
55
|
+
return { frontmatter: {}, body: text, hasFrontmatter: false };
|
|
56
|
+
}
|
|
57
|
+
const parsed = safeYamlLoad(m[1], filename);
|
|
58
|
+
return {
|
|
59
|
+
frontmatter: normaliseFrontmatter(parsed),
|
|
60
|
+
body: m[2] ?? '',
|
|
61
|
+
hasFrontmatter: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render a markdown document from a frontmatter object and a body string.
|
|
67
|
+
* Uses `yaml.dump` — callers that need a specific key order (e.g. edge type
|
|
68
|
+
* files where `sources`/`targets` are rendered as inline YAML arrays) should
|
|
69
|
+
* build the YAML text themselves and wrap with `---\n...\n---\n`.
|
|
70
|
+
*/
|
|
71
|
+
export function renderMarkdown(frontmatter, body = '') {
|
|
72
|
+
const yamlText = yaml.dump(frontmatter, { lineWidth: -1, noRefs: true }).replace(/\n$/, '');
|
|
73
|
+
const prefix = body.startsWith('\n') ? '' : '\n';
|
|
74
|
+
return `---\n${yamlText}\n---\n${prefix}${body}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function assertFiniteNumber(value, path) {
|
|
2
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
3
|
+
throw new Error(`NDJSON: non-finite number at ${path}`);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function assertFiniteArray(value, path) {
|
|
8
|
+
value.forEach((v, i) => assertFiniteNumbers(v, `${path}[${i}]`));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertFiniteObject(value, path) {
|
|
12
|
+
for (const [k, v] of Object.entries(value)) assertFiniteNumbers(v, `${path}.${k}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertFiniteNumbers(value, path) {
|
|
16
|
+
assertFiniteNumber(value, path);
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
assertFiniteArray(value, path);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (value && typeof value === 'object') {
|
|
22
|
+
assertFiniteObject(value, path);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalise(value) {
|
|
27
|
+
if (Array.isArray(value)) return value.map(canonicalise);
|
|
28
|
+
if (value && typeof value === 'object') {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const k of Object.keys(value).sort()) out[k] = canonicalise(value[k]);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function serialiseLine(row) {
|
|
37
|
+
assertFiniteNumbers(row, 'row');
|
|
38
|
+
return JSON.stringify(canonicalise(row));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function compareEntity(a, b) {
|
|
42
|
+
if (a.name < b.name) return -1;
|
|
43
|
+
if (a.name > b.name) return 1;
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const EDGE_KEY_ORDER = ['from_type', 'from_name', 'to_type', 'to_name'];
|
|
48
|
+
function compareEdge(a, b) {
|
|
49
|
+
for (const k of EDGE_KEY_ORDER) {
|
|
50
|
+
if (a[k] < b[k]) return -1;
|
|
51
|
+
if (a[k] > b[k]) return 1;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function serialiseEntityRows(rows) {
|
|
57
|
+
if (rows.length === 0) return '';
|
|
58
|
+
const sorted = [...rows].sort(compareEntity);
|
|
59
|
+
return sorted.map(serialiseLine).join('\n') + '\n';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function serialiseEdgeRows(rows) {
|
|
63
|
+
if (rows.length === 0) return '';
|
|
64
|
+
const sorted = [...rows].sort(compareEdge);
|
|
65
|
+
return sorted.map(serialiseLine).join('\n') + '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseLines(text) {
|
|
69
|
+
const out = [];
|
|
70
|
+
const lines = text.split('\n');
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
if (line.trim() === '') continue;
|
|
74
|
+
try {
|
|
75
|
+
out.push(JSON.parse(line));
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new Error(`NDJSON: invalid JSON at line ${i + 1}: ${err.message}`, { cause: err });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const parseEntityRows = parseLines;
|
|
84
|
+
export const parseEdgeRows = parseLines;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function memoryPaths(foundryDir) {
|
|
4
|
+
const root = join(foundryDir, 'memory');
|
|
5
|
+
const entitiesDir = join(root, 'entities');
|
|
6
|
+
const edgesDir = join(root, 'edges');
|
|
7
|
+
// Phase 2 moved relations out of foundry/ to foundry-memory/relations/
|
|
8
|
+
// (sibling to foundry/). Derive from foundryDir using '..' to go up one level.
|
|
9
|
+
const relationsDir = join(foundryDir, '..', 'foundry-memory', 'relations');
|
|
10
|
+
const extractorsDir = join(root, 'extractors');
|
|
11
|
+
return {
|
|
12
|
+
root,
|
|
13
|
+
config: join(root, 'config.md'),
|
|
14
|
+
schema: join(root, 'schema.json'),
|
|
15
|
+
entitiesDir,
|
|
16
|
+
edgesDir,
|
|
17
|
+
relationsDir,
|
|
18
|
+
extractorsDir,
|
|
19
|
+
db: join(root, 'memory.db'),
|
|
20
|
+
entityTypeFile: (name) => join(entitiesDir, `${name}.md`),
|
|
21
|
+
edgeTypeFile: (name) => join(edgesDir, `${name}.md`),
|
|
22
|
+
relationFile: (name) => join(relationsDir, `${name}.ndjson`),
|
|
23
|
+
extractorFile: (name) => join(extractorsDir, `${name}.md`),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function filterValidTypes(types, vocabulary) {
|
|
2
|
+
const result = new Set();
|
|
3
|
+
for (const t of types ?? []) {
|
|
4
|
+
if (vocabulary.entities[t]) result.add(t);
|
|
5
|
+
}
|
|
6
|
+
return result;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolvePermissions({ cycleFrontmatter, vocabulary }) {
|
|
10
|
+
const mem = cycleFrontmatter?.memory;
|
|
11
|
+
if (!mem || typeof mem !== 'object') {
|
|
12
|
+
return { enabled: false, readTypes: new Set(), writeTypes: new Set(), vocabulary };
|
|
13
|
+
}
|
|
14
|
+
const readTypes = filterValidTypes(mem.read, vocabulary);
|
|
15
|
+
const writeTypes = filterValidTypes(mem.write, vocabulary);
|
|
16
|
+
return { enabled: readTypes.size > 0 || writeTypes.size > 0, readTypes, writeTypes, vocabulary };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function endpointInSet(endpointSpec, set) {
|
|
20
|
+
if (endpointSpec === 'any') return set.size > 0;
|
|
21
|
+
return endpointSpec.some((t) => set.has(t));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function checkEntityRead(perms, type) { return perms.readTypes.has(type); }
|
|
25
|
+
export function checkEntityWrite(perms, type) { return perms.writeTypes.has(type); }
|
|
26
|
+
|
|
27
|
+
export function checkEdgeRead(perms, edgeType) {
|
|
28
|
+
const edge = perms.vocabulary.edges[edgeType];
|
|
29
|
+
if (!edge) return false;
|
|
30
|
+
return endpointInSet(edge.sources, perms.readTypes)
|
|
31
|
+
|| endpointInSet(edge.targets, perms.readTypes)
|
|
32
|
+
|| endpointInSet(edge.sources, perms.writeTypes)
|
|
33
|
+
|| endpointInSet(edge.targets, perms.writeTypes);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function checkEdgeWrite(perms, edgeType) {
|
|
37
|
+
const edge = perms.vocabulary.edges[edgeType];
|
|
38
|
+
if (!edge) return false;
|
|
39
|
+
return endpointInSet(edge.sources, perms.writeTypes)
|
|
40
|
+
|| endpointInSet(edge.targets, perms.writeTypes);
|
|
41
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { checkEdgeRead, checkEdgeWrite } from './permissions.js';
|
|
2
|
+
|
|
3
|
+
function entityBlock(name, typeDef, isWrite) {
|
|
4
|
+
return [
|
|
5
|
+
`### entity: \`${name}\` (${isWrite ? 'read+write' : 'read-only'})`,
|
|
6
|
+
'',
|
|
7
|
+
typeDef.body ?? '(no description)',
|
|
8
|
+
'',
|
|
9
|
+
].join('\n');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function edgeBlock(name, edgeDef, canWrite) {
|
|
13
|
+
const renderList = (v) => v === 'any' ? 'any' : `[${v.join(', ')}]`;
|
|
14
|
+
return [
|
|
15
|
+
`### edge: \`${name}\` (${canWrite ? 'read+write' : 'read-only'})`,
|
|
16
|
+
`Sources: ${renderList(edgeDef.sources)}. Targets: ${renderList(edgeDef.targets)}.`,
|
|
17
|
+
'',
|
|
18
|
+
edgeDef.body ?? '(no description)',
|
|
19
|
+
'',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractorBlock({ name, body }) {
|
|
24
|
+
return [
|
|
25
|
+
`### extractor: \`${name}\``,
|
|
26
|
+
'',
|
|
27
|
+
(body && body.trim()) ? body.trim() : '(no description)',
|
|
28
|
+
'',
|
|
29
|
+
].join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderToolLines(embeddingsEnabled, hasWriteTypes) {
|
|
33
|
+
const lines = [
|
|
34
|
+
'### Memory tools available to you',
|
|
35
|
+
'',
|
|
36
|
+
'- `foundry_memory_get(type, name)` — fetch an entity (null if absent).',
|
|
37
|
+
'- `foundry_memory_list(type)` — list all entities of a type.',
|
|
38
|
+
'- `foundry_memory_neighbours(type, name, depth?, edge_types?)` — bounded graph traversal.',
|
|
39
|
+
'- `foundry_memory_query(datalog)` — arbitrary read-only Cozo Datalog.',
|
|
40
|
+
];
|
|
41
|
+
if (embeddingsEnabled) {
|
|
42
|
+
lines.push('- `foundry_memory_search(type, query, limit?)` — semantic (vector) search over entities of a type.');
|
|
43
|
+
}
|
|
44
|
+
if (hasWriteTypes) {
|
|
45
|
+
lines.push('- `foundry_memory_put(type, name, value)` — upsert an entity (≤4KB value).');
|
|
46
|
+
lines.push('- `foundry_memory_relate(from_type, from_name, edge_type, to_type, to_name)` — upsert an edge.');
|
|
47
|
+
lines.push('- `foundry_memory_unrelate(...)` — delete an edge.');
|
|
48
|
+
}
|
|
49
|
+
lines.push('');
|
|
50
|
+
lines.push('Writes to types outside your permissions are rejected.');
|
|
51
|
+
lines.push('');
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderEntityLines(allTypes, vocabulary, writeTypes) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
for (const name of [...allTypes].sort()) {
|
|
58
|
+
lines.push(entityBlock(name, vocabulary.entities[name], writeTypes.has(name)));
|
|
59
|
+
}
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderEdgeLines(permissions, vocabulary) {
|
|
64
|
+
const visibleEdges = Object.keys(vocabulary.edges)
|
|
65
|
+
.filter((n) => checkEdgeRead(permissions, n))
|
|
66
|
+
.sort();
|
|
67
|
+
if (visibleEdges.length === 0) return [];
|
|
68
|
+
const lines = ['Edges visible to this cycle:', ''];
|
|
69
|
+
for (const name of visibleEdges) {
|
|
70
|
+
lines.push(edgeBlock(name, vocabulary.edges[name], checkEdgeWrite(permissions, name)));
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderExtractorSection(extractors) {
|
|
76
|
+
if (!Array.isArray(extractors) || extractors.length === 0) return [];
|
|
77
|
+
const lines = ['## Extractors', ''];
|
|
78
|
+
lines.push('The following extractors populate this cycle\'s memory during the assay stage. Their prose briefs describe what gets captured:');
|
|
79
|
+
lines.push('');
|
|
80
|
+
for (const ex of extractors) {
|
|
81
|
+
lines.push(extractorBlock(ex));
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function renderMemoryPrompt({ permissions, schema, extractors }) {
|
|
87
|
+
if (!permissions.enabled) return '';
|
|
88
|
+
|
|
89
|
+
const { readTypes, writeTypes, vocabulary } = permissions;
|
|
90
|
+
const embeddingsEnabled = Boolean(schema?.embeddings?.dimensions);
|
|
91
|
+
const allTypes = new Set([...readTypes, ...writeTypes]);
|
|
92
|
+
|
|
93
|
+
const lines = [
|
|
94
|
+
'## Flow memory',
|
|
95
|
+
'',
|
|
96
|
+
'You have access to a typed, graph-shaped memory store. Use it to save what you learn and to query what previous cycles learned.',
|
|
97
|
+
'',
|
|
98
|
+
'Types visible to this cycle:',
|
|
99
|
+
'',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
lines.push(...renderEntityLines(allTypes, vocabulary, writeTypes));
|
|
103
|
+
lines.push(...renderEdgeLines(permissions, vocabulary));
|
|
104
|
+
|
|
105
|
+
lines.push(...renderToolLines(embeddingsEnabled, writeTypes.size > 0));
|
|
106
|
+
lines.push(...renderExtractorSection(extractors));
|
|
107
|
+
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|