Compare commits

...

6 Commits
1.2.2 ... 1.2.7

Author SHA1 Message Date
8dfcd06752 More memory fixes
All checks were successful
Publish Library / Build NPM Project (push) Successful in 43s
Publish Library / Tag Version (push) Successful in 14s
2026-07-29 22:11:09 -04:00
14f6cdd313 Personal file memory organization instructions
All checks were successful
Publish Library / Build NPM Project (push) Successful in 33s
Publish Library / Tag Version (push) Successful in 12s
2026-07-27 22:47:48 -04:00
73d6ee0f2a Personal file memory organization instructions
All checks were successful
Publish Library / Build NPM Project (push) Successful in 45s
Publish Library / Tag Version (push) Successful in 12s
2026-07-27 22:39:06 -04:00
bee4085666 updatememory awaits full result
Some checks failed
Publish Library / Tag Version (push) Has been cancelled
Publish Library / Build NPM Project (push) Has been cancelled
2026-07-27 22:34:36 -04:00
3b5c71de7c Improved memory management
All checks were successful
Publish Library / Build NPM Project (push) Successful in 40s
Publish Library / Tag Version (push) Successful in 14s
2026-07-27 20:10:09 -04:00
8229e02a52 Improved memory management
All checks were successful
Publish Library / Build NPM Project (push) Successful in 44s
Publish Library / Tag Version (push) Successful in 11s
2026-07-27 14:25:24 -04:00
9 changed files with 660 additions and 417 deletions

234
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@ztimson/ai-utils",
"version": "1.0.6",
"version": "1.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ztimson/ai-utils",
"version": "1.0.6",
"version": "1.2.6",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.102.0",
@@ -57,34 +57,38 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"version": "2.0.0-alpha.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
"integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"@emnapi/wasi-threads": "2.0.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
"integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
"version": "2.0.0-alpha.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
"integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
"integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -573,6 +577,16 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-wasm32/node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
@@ -681,22 +695,25 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz",
"integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.3"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=23.5.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1"
"@emnapi/core": "^2.0.0-alpha.3",
"@emnapi/runtime": "^2.0.0-alpha.3"
}
},
"node_modules/@oxc-project/types": {
@@ -1007,6 +1024,18 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
@@ -1018,6 +1047,17 @@
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
@@ -1408,18 +1448,18 @@
"license": "MIT"
},
"node_modules/@ztimson/utils": {
"version": "0.29.5",
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.5.tgz",
"integrity": "sha512-8mUuhi//3agwrueR006emOvJu1JXxVEryJmD3nkEmK4yQ9qS24oZijdoaiLRid/6xc75j3Fk6YjmnYmjq61iPQ==",
"version": "0.29.7",
"resolved": "https://registry.npmjs.org/@ztimson/utils/-/utils-0.29.7.tgz",
"integrity": "sha512-cjQ9+RjC5X7gKNA/hJHDf7OtyYCa+5E0PDc76lIaATwNAxXCSx2IO9r2wHiHtZGV5bldjnFmw7aOV8Jmq7SgKQ==",
"license": "MIT",
"dependencies": {
"var-persist": "^1.0.1"
}
},
"node_modules/acorn": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1504,9 +1544,9 @@
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz",
"integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2009,9 +2049,9 @@
"license": "MIT"
},
"node_modules/exsolve": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz",
"integrity": "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
"integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
"dev": true,
"license": "MIT"
},
@@ -2382,9 +2422,9 @@
"license": "MIT"
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
@@ -2398,23 +2438,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
"lightningcss-android-arm64": "1.33.0",
"lightningcss-darwin-arm64": "1.33.0",
"lightningcss-darwin-x64": "1.33.0",
"lightningcss-freebsd-x64": "1.33.0",
"lightningcss-linux-arm-gnueabihf": "1.33.0",
"lightningcss-linux-arm64-gnu": "1.33.0",
"lightningcss-linux-arm64-musl": "1.33.0",
"lightningcss-linux-x64-gnu": "1.33.0",
"lightningcss-linux-x64-musl": "1.33.0",
"lightningcss-win32-arm64-msvc": "1.33.0",
"lightningcss-win32-x64-msvc": "1.33.0"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
"integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
"cpu": [
"arm64"
],
@@ -2433,9 +2473,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
"integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
"cpu": [
"arm64"
],
@@ -2454,9 +2494,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
"integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
"cpu": [
"x64"
],
@@ -2475,9 +2515,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
"integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
"cpu": [
"x64"
],
@@ -2496,9 +2536,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
"integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
"cpu": [
"arm"
],
@@ -2517,9 +2557,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
"integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
"cpu": [
"arm64"
],
@@ -2541,9 +2581,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
"integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
"cpu": [
"arm64"
],
@@ -2565,9 +2605,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
"integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
"cpu": [
"x64"
],
@@ -2589,9 +2629,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
"integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
"cpu": [
"x64"
],
@@ -2613,9 +2653,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
"integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
"cpu": [
"arm64"
],
@@ -2634,9 +2674,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
"cpu": [
"x64"
],
@@ -2794,9 +2834,9 @@
}
},
"node_modules/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
"dev": true,
"license": "MIT"
},
@@ -2971,9 +3011,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
@@ -3092,9 +3132,9 @@
"license": "MIT"
},
"node_modules/openai": {
"version": "6.46.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.46.0.tgz",
"integrity": "sha512-DFg6jEPT2RO+oAyXtddeUJU8zkGy1OQ1AjGzNIJUMQG03TTqvCpy9tBpQ+2VVVnvrl3E56F8GEin2JYtWpITtA==",
"version": "6.49.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.49.0.tgz",
"integrity": "sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/credential-provider-node": ">=3.972.0 <4",
@@ -3232,9 +3272,9 @@
"license": "MIT"
},
"node_modules/postcss": {
"version": "8.5.17",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.17.tgz",
"integrity": "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w==",
"version": "8.5.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
"dev": true,
"funding": [
{
@@ -3252,7 +3292,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.12",
"nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -3787,9 +3827,9 @@
"license": "MIT"
},
"node_modules/undici": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -3981,16 +4021,16 @@
}
},
"node_modules/vite": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
"version": "8.1.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.5",
"postcss": "^8.5.16",
"rolldown": "~1.1.4",
"postcss": "^8.5.17",
"rolldown": "~1.1.5",
"tinyglobby": "^0.2.17"
},
"bin": {

View File

@@ -1,6 +1,6 @@
{
"name": "@ztimson/ai-utils",
"version": "1.2.2",
"version": "1.2.7",
"description": "AI Utility library",
"author": "Zak Timson",
"license": "MIT",

View File

@@ -3,6 +3,8 @@ export * from './antrhopic';
export * from './audio';
export * from './llm';
export * from './memory';
export * from './memory-cache';
export * from './memory-graph';
export * from './open-ai';
export * from './provider';
export * from './tools';

View File

@@ -1,12 +1,13 @@
import {AbortablePromise, Ai} from './ai.ts';
import {Anthropic} from './antrhopic.ts';
import {MemoryCache} from './memory-cache.ts';
import {OpenAi} from './open-ai.ts';
import {LLMProvider} from './provider.ts';
import {AiTool, AiToolArg} from './tools.ts';
import {fileURLToPath} from 'url';
import {dirname, join} from 'path';
import {spawn} from 'node:child_process';
import {Memory, MemoryCache, MemoryManager} from './memory.ts';
import {Memory, MemoryManager} from './memory.ts';
export type AnthropicConfig = {proto: 'anthropic', token: string};
export type OpenAiConfig = {proto: 'openai', host?: string, token: string};
@@ -143,7 +144,7 @@ class LLM {
return {
prompt: `You have access to the following skill documents, use \`read_skill\` to access them:\n${list}`,
tools: [{
name: 'read_skill',
name: 'skill_read',
description: 'Read the full content of a skill/knowledge document',
args: {
name: {type: 'string', description: 'Exact skill name', required: true}
@@ -167,8 +168,14 @@ class LLM {
}
const m = options.model || this.defaultModel;
if(!this.models[m]) throw new Error(`Model does not exist: ${m}`);
let abort = () => {};
return Object.assign(new Promise<string>(async res => {
let request: AbortablePromise<string> | null = null;
let aborted = false;
const abort = () => {
aborted = true;
request?.abort?.();
};
const promise = (async () => {
let tools: AiTool[] = options.tools || this.ai.options.llm?.tools || [];
const prompts: string[] = [];
let history = options.history || [];
@@ -192,6 +199,7 @@ class LLM {
// Memory
if (options.memory) {
const mems = options.memory instanceof MemoryCache ? options.memory.memories : options.memory;
if(mems.length) {
const relevant = await this.memoryManager.recollect(message, options.memory, 5);
prompts.unshift(`You have access to the following memory files:
${mems.map(m => `- ${m.name}: ${m.description}`).join('\n')}
@@ -205,9 +213,13 @@ ${r.content}
` : ''}`.trim());
tools.push(this.memoryManager.tools.read(options.memory));
}
}
if(aborted) throw Object.assign(new Error('Aborted'), {name: 'AbortError'});
prompts.unshift(options.system || this.ai.options.llm?.system || '');
const resp = await this.models[m].ask(message, {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
request = this.models[m].ask(message, {...options, tools, system: prompts.filter(Boolean).join('\n\n')});
const resp = await request;
// Trim memory injections from history
if(options.memory) {
@@ -221,16 +233,18 @@ ${r.content}
if(options.history) options.history.splice(0, options.history.length, ...compressed);
}
return res(resp);
}), {abort});
return resp;
})();
return Object.assign(promise, {abort});
}
/**
* Digest full conversation history into memory documents.
* Call on session end to persist the conversation.
*/
async updateMemory(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest = {}): Promise<void> {
await this.memoryManager.memorize(history, memories, {model: this.defaultModel, ...options});
async updateMemory(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest = {}): Promise<Memory[]> {
return this.memoryManager.memorize(history, memories, {model: this.defaultModel, ...options});
}
/**

59
src/memory-cache.ts Normal file
View File

@@ -0,0 +1,59 @@
import {KDPoint, KDTree} from './kd-tree.ts';
import {Memory, MemoryRef} from './memory.ts';
export class MemoryCache {
private tree: KDTree<MemoryRef>;
public memories: Memory[];
get length() { return this.memories.length; }
constructor(memories: Memory[]) {
this.memories = memories;
this.tree = this.buildTree();
}
private buildTree(): KDTree<MemoryRef> {
const embedded = this.memories.filter(m => m.embedding?.length);
if (!embedded.length) return new KDTree<MemoryRef>(0);
const dims = embedded[0].embedding.length;
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
vector: m.embedding,
payload: {name: m.name, description: m.description},
}));
return new KDTree<MemoryRef>(dims, 'cosine', points);
}
search(query: number[], limit: number): MemoryRef[] {
const results = this.tree.knn(query, limit);
return results.map(r => r.point.payload);
}
add(memory: Memory): void {
this.memories.push(memory);
this.rebuild();
}
update(memory: Memory): void {
const idx = this.memories.findIndex(m => m.name === memory.name);
if (idx !== -1) {
this.memories[idx] = memory;
} else {
this.memories.push(memory);
}
this.rebuild();
}
remove(name: string): void {
const idx = this.memories.findIndex(m => m.name === name);
if (idx !== -1) {
this.memories.splice(idx, 1);
this.rebuild();
}
}
rebuild(): void {
this.tree = this.buildTree();
}
}

69
src/memory-graph.ts Normal file
View File

@@ -0,0 +1,69 @@
import {MemoryCache} from './memory-cache.ts';
import {extractMetadata, Memory, MemoryNode} from './memory.ts';
export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
const mems = memories instanceof MemoryCache ? memories.memories : memories;
const nameSet = new Set(mems.map(m => m.name));
const ghosts = new Set<string>();
const nodes: MemoryNode[] = mems.map(m => {
const {links, backlinks} = extractMetadata(m.content);
return {
name: m.name,
missing: false,
links,
backlinks,
};
});
for (const node of nodes) {
for (const link of node.links) {
if (!nameSet.has(link)) ghosts.add(link);
}
}
return [
...nodes,
...[...ghosts].map(name => ({
name,
missing: true,
links: [],
backlinks: nodes
.filter(n => n.links.includes(name))
.map(n => n.name),
}))
];
}
export function renderMemoryGraph(nodes) {
if (!nodes.length) return 'No memories yet.';
const groups = new Map();
for (const node of nodes) {
const [prefix, ...rest] = node.name.split('/');
const group = rest.length ? prefix : 'Root';
const label = rest.length ? rest.join('/') : node.name;
if (!groups.has(group)) groups.set(group, []);
groups.get(group).push({...node, label});
}
const ghostCount = nodes.filter(n => n.missing).length;
const lines = [`Memory Graph (${nodes.length} nodes, ${ghostCount} ghost${ghostCount === 1 ? '' : 's'})`, ''];
for (const group of [...groups.keys()].sort()) {
const items = groups.get(group).sort((a, b) => a.label.localeCompare(b.label));
lines.push(`${group}/`);
items.forEach((n, i) => {
const last = i === items.length - 1;
const branch = last ? '└─' : '├─';
const pad = last ? ' ' : '│ ';
const tag = n.missing ? ' (ghost)' : '';
lines.push(` ${branch} ${n.label}${tag}`);
if (n.links.length) lines.push(` ${pad}${n.links.join(', ')}`);
if (n.backlinks.length) lines.push(` ${pad}${n.backlinks.join(', ')}`);
});
lines.push('');
}
return lines.join('\n').trimEnd();
}

View File

@@ -1,26 +1,23 @@
import {LLMRequest, LLMMessage} from './llm.ts';
import {MemoryCache} from './memory-cache.ts';
import {AiTool} from './tools.ts';
import {KDTree, KDPoint} from './kd-tree.ts';
export type Memory = {
name: string;
description: string;
content: string;
embedding: number[];
links: string[];
backlinks: string[];
}
type MemoryRef = {
export type MemoryRef = {
name: string;
description: string;
}
type FactBucket = {
export type FactBucket = {
subject: string;
facts: string[];
}
// In memory.ts - replace findGhostNodes with this:
export type MemoryNode = {
name: string;
@@ -29,50 +26,36 @@ export type MemoryNode = {
backlinks: string[];
}
export function buildMemoryGraph(memories: Memory[] | MemoryCache): MemoryNode[] {
const mems = memories instanceof MemoryCache ? memories.memories : memories;
const nameSet = new Set(mems.map(m => m.name));
const ghosts = new Set<string>();
// Collect all ghost references
for (const m of mems) {
for (const link of m.links) {
if (!nameSet.has(link)) ghosts.add(link);
}
}
// Build node list: real nodes + ghost nodes
return [
...mems.map(m => ({
name: m.name,
missing: false,
links: m.links,
backlinks: m.backlinks,
})),
...[...ghosts].map(name => ({
name,
missing: true,
links: [],
backlinks: mems
.filter(m => m.links.includes(name))
.map(m => m.name),
}))
];
}
function extractLinks(content: string): string[] {
if(!content) return [];
const matches = content.matchAll(/\[\[([^\]]+)\]\]/g);
return [...new Set([...matches].map(m => m[1].trim()))];
}
function rebuildBacklinks(memories: Memory[]): void {
for (const m of memories) m.backlinks = [];
for (const m of memories) {
for (const link of m.links) {
const target = memories.find(t => t.name === link);
if (target) target.backlinks.push(m.name);
export function extractMetadata(content: string): {links: string[], backlinks: string[]} {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {links: [], backlinks: []};
const fm = match[1];
const getList = (key: string): string[] => {
const m = fm.match(new RegExp(`^${key}:\\s*\\[(.*)\\]$`, 'm'));
if (!m || !m[1].trim()) return [];
return m[1].split(',').map(s => s.trim().replace(/^"|"$/g, '')).filter(Boolean);
};
return {
links: getList('links'),
backlinks: getList('backlinks'),
};
}
function dedupeFacts(facts: string[]): string[] {
const seen = new Map<string, string>();
for (const f of facts) {
const clean = f.trim();
if (clean) seen.set(clean.toLowerCase(), clean);
}
return [...seen.values()];
}
function cosineDistance(a: number[], b: number[]): number {
@@ -86,62 +69,38 @@ function cosineDistance(a: number[], b: number[]): number {
return denom === 0 ? 1 : 1 - dot / denom;
}
export class MemoryCache {
private tree: KDTree<MemoryRef>;
public memories: Memory[];
constructor(memories: Memory[]) {
this.memories = memories;
this.tree = this.buildTree();
function getWeekMonday(date: Date = new Date()): string {
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
const day = d.getUTCDay();
const diff = day === 0 ? -6 : 1 - day;
d.setUTCDate(d.getUTCDate() + diff);
return d.toISOString().slice(0, 10);
}
private buildTree(): KDTree<MemoryRef> {
const embedded = this.memories.filter(m => m.embedding?.length);
if(!embedded.length) return new KDTree<MemoryRef>(0);
const dims = embedded[0].embedding.length;
const points: KDPoint<MemoryRef>[] = embedded.map(m => ({
vector: m.embedding,
payload: {name: m.name, description: m.description},
}));
return new KDTree<MemoryRef>(dims, 'cosine', points);
function getWeekSunday(monday: string): string {
const d = new Date(`${monday}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + 6);
return d.toISOString().slice(0, 10);
}
search(query: number[], limit: number): MemoryRef[] {
const results = this.tree.knn(query, limit);
return results.map(r => r.point.payload);
}
add(memory: Memory): void {
this.memories.push(memory);
this.rebuild();
}
update(memory: Memory): void {
const idx = this.memories.findIndex(m => m.name === memory.name);
if (idx !== -1) {
this.memories[idx] = memory;
} else {
this.memories.push(memory);
}
this.rebuild();
}
rebuild(): void {
this.tree = this.buildTree();
}
rebuildLinks(): void {
rebuildBacklinks(this.memories);
}
}
export class MemoryManager {
private pendingMemorizations = new Map<string, {
memories: Memory[] | MemoryCache,
tempMemoryName: string,
timestamp: number,
}>();
private queues = new Map<string, {
pending: string[],
request: {abort?: () => void} | null,
task: Promise<void>,
}>();
tools = {
read: (memories: Memory[] | MemoryCache): AiTool => ({
name: 'read_memory',
name: 'memory_recall',
description: 'Read the full content of a memory document',
args: {
name: {type: 'string', description: 'Exact memory name', required: true},
@@ -150,49 +109,84 @@ export class MemoryManager {
const mems = memories instanceof MemoryCache ? memories.memories : memories;
const mem = mems.find(m => m.name === args.name);
if (!mem) return 'Document not found';
return this.formatMemory(mem);
}
return mem.content;
},
}),
forget: (memories: Memory[] | MemoryCache): AiTool => ({
name: 'memory_forget',
description: 'Permanently delete a memory document and clean up all references to it',
args: {
name: {type: 'string', description: 'Exact memory name to forget', required: true}
},
fn: (args: any) => {
const result = this.forget(args.name, memories);
return result ? `Forgotten: ${args.name}` : `Not found: ${args.name}`;
},
}),
};
constructor(private llm: any) {}
private async createTempMemory(conversation: string): Promise<Memory> {
const timestamp = Date.now();
const content = `---
name: _temp_${timestamp}
description: Temporary memory - processing in background
tags: [_temporary]
links: []
backlinks: []
modified: ${new Date().toISOString()}
---
# Recent Conversation (Processing)
${conversation}`;
const [e] = await this.llm.embedding(content);
return {
name: `_temp_${timestamp}`,
description: 'Temporary memory - processing in background',
content,
embedding: e?.embedding || [],
};
}
forget(name: string, memories: Memory[] | MemoryCache): boolean {
const mem = memories instanceof MemoryCache ? memories.memories : memories;
const idx = mem.findIndex(m => m.name === name);
if (idx === -1) return false;
for (const node of mem) {
const {links, backlinks} = extractMetadata(node.content);
const newBacklinks = backlinks.filter(b => b !== name);
const newLinks = links.filter(l => l !== name);
if (newBacklinks.length !== backlinks.length || newLinks.length !== links.length) {
node.content = this.updateFrontmatter(node.content, {
links: newLinks,
backlinks: newBacklinks,
});
}
}
mem.splice(idx, 1);
if (memories instanceof MemoryCache) memories.rebuild();
return true;
}
private cosineSearch(query: number[], memories: Memory[], limit: number): MemoryRef[] {
const scored = memories
.filter(m => m.embedding?.length)
.map(m => ({
ref: {name: m.name, description: m.description},
distance: cosineDistance(query, m.embedding)
distance: cosineDistance(query, m.embedding),
}))
.sort((a, b) => a.distance - b.distance)
.slice(0, limit);
return scored.map(s => s.ref);
}
private createNode(name: string, memories: Memory[]): Memory {
const existing = memories.find(m => m.name === name);
if(existing) return existing;
return {
name,
description: '',
content: '',
embedding: [],
links: [],
backlinks: [],
};
}
private formatMemory(mem: Memory): string {
return [
`# ${mem.name}`,
mem.description ? `> ${mem.description}` : '',
mem.links.length ? `**Links:** ${mem.links.map(l => `[[${l}]]`).join(', ')}` : '',
mem.backlinks.length ? `**Referenced by:** ${mem.backlinks.map(l => `[[${l}]]`).join(', ')}` : '',
'',
mem.content,
].filter(l => l !== undefined).join('\n');
}
private listNodes(memories: Memory[]): MemoryRef[] {
return memories.map(m => ({name: m.name, description: m.description}));
}
@@ -200,6 +194,7 @@ export class MemoryManager {
async recollect(query: string, memories: Memory[] | MemoryCache, limit = 5, graphDepth = 1): Promise<Memory[]> {
const mem: Memory[] = memories instanceof MemoryCache ? memories.memories : memories;
if (!mem.length) return [];
const [e] = await this.llm.embedding(query);
if (!e) return [];
@@ -208,7 +203,6 @@ export class MemoryManager {
else vectorResults = this.cosineSearch(e.embedding, mem, limit);
const found = new Set<string>(vectorResults.map(r => r.name));
// Graph expansion
if (graphDepth > 0) {
const frontier = [...found];
for (let depth = 0; depth < graphDepth; depth++) {
@@ -216,7 +210,8 @@ export class MemoryManager {
for (const name of frontier) {
const node = mem.find(m => m.name === name);
if (!node) continue;
for(const link of node.links) {
const {links} = extractMetadata(node.content);
for (const link of links) {
if (!found.has(link) && mem.find(m => m.name === link)) {
found.add(link);
next.push(link);
@@ -234,187 +229,256 @@ export class MemoryManager {
return ordered.map(n => mem.find(m => m.name === n)!).filter(Boolean);
}
async memorize(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest): Promise<void> {
const mem = memories instanceof MemoryCache ? memories.memories : memories;
async memorize(history: LLMMessage[], memories: Memory[] | MemoryCache, options: LLMRequest): Promise<Memory[]> {
const conversation = history
.filter(h => h.role === 'user' || h.role === 'assistant')
.map(h => `[${h.role}]: ${h.content}`).join('\n\n').trim();
if(!conversation) return;
if(!conversation) return [];
const buckets = await this.factAgent(conversation, mem, options);
const trackingId = `${Date.now()}_${Math.random()}`;
const tempMemory = await this.createTempMemory(conversation);
const mem = memories instanceof MemoryCache ? memories.memories : memories;
mem.push(tempMemory);
if (memories instanceof MemoryCache) memories.rebuild();
this.pendingMemorizations.set(trackingId, {
memories,
tempMemoryName: tempMemory.name,
timestamp: Date.now(),
});
try {
await this._memorizeBackground(conversation, memories, options, tempMemory.name);
const finalMem = memories instanceof MemoryCache ? memories.memories : memories;
return finalMem.filter(m => !m.name.startsWith('_temp_'));
} finally {
const pending = this.pendingMemorizations.get(trackingId);
if (pending) {
const cleanMem = pending.memories instanceof MemoryCache
? pending.memories.memories
: pending.memories;
const idx = cleanMem.findIndex(m => m.name === pending.tempMemoryName);
if (idx !== -1) cleanMem.splice(idx, 1);
if (pending.memories instanceof MemoryCache) pending.memories.rebuild();
}
this.pendingMemorizations.delete(trackingId);
}
}
private async _memorizeBackground(conversation: string, memories: Memory[] | MemoryCache, options: LLMRequest, tempName: string): Promise<void> {
const mem = memories instanceof MemoryCache ? memories.memories : memories;
const monday = getWeekMonday();
const sunday = getWeekSunday(monday);
const buckets = await this.factAgent(conversation, mem, options, monday);
if(!buckets.length) return;
await Promise.all(buckets.map(async bucket => {
const node = await this.organizingAgent(bucket, mem, options);
if(!mem.find(m => m.name === node.name)) mem.push(node);
await this.docAgent(node, bucket, mem, options);
}));
// Rebuild indexes
if (memories instanceof MemoryCache) {
memories.rebuildLinks();
memories.rebuild();
} else {
rebuildBacklinks(mem);
const jobs = [...buckets].map(({subject, facts}) => {
let node = mem.find(m => m.name === subject);
if(!node) {
node = {name: subject, description: '', content: '', embedding: [],};
mem.push(node);
}
const week = subject.startsWith('Journal/') ? {monday, sunday} : undefined;
return this.enqueue(node, facts, mem, options, tempName, week);
});
await Promise.all(jobs);
}
private async docAgent(node: Memory, bucket: FactBucket, memories: Memory[], options: LLMRequest): Promise<void> {
let finalContent = node.content;
/**
* Coalescing queue: if a doc is already compiling, abort the in-flight run, merge its
* facts with the new ones and restart. Never blocks a pending update, never drops facts.
*/
private enqueue(node: Memory, facts: string[], memories: Memory[] | MemoryCache, options: LLMRequest, tempName: string, week?: {monday: string, sunday: string}): Promise<void> {
const key = node.name;
const existing = this.queues.get(key);
if (existing) {
existing.pending.push(...facts);
existing.request?.abort?.();
return existing.task;
}
await this.llm.ask(
`New facts to integrate:\n${bucket.facts.map(f => `- ${f}`).join('\n')}`,
{
const entry: {pending: string[], request: {abort?: () => void} | null, task: Promise<void>} = {pending: [...facts], request: null, task: Promise.resolve()};
this.queues.set(key, entry);
const m = memories instanceof MemoryCache ? memories.memories : memories;
entry.task = (async () => {
while (entry.pending.length) {
const batch = dedupeFacts(entry.pending.splice(0, entry.pending.length));
const written = await this.docAgent(node, batch, m, options, tempName, week, entry);
if (!written) entry.pending.unshift(...batch);
}
})().finally(() => {
this.queues.delete(key);
if(!this.queues.size && memories instanceof MemoryCache) memories.rebuild();
});
return entry.task;
}
private buildHeader(node: Memory, week?: {monday: string, sunday: string}, links: string[] = [], backlinks: string[] = []): string {
const tags = node.name.split('/')[0]?.toLowerCase();
const lines = [
'---',
`name: ${node.name}`,
`description: ${node.description || ''}`,
tags ? `tags: [${tags}]` : '',
links.length ? `links: [${links.map(l => `"${l}"`).join(', ')}]` : 'links: []',
backlinks.length ? `backlinks: [${backlinks.map(l => `"${l}"`).join(', ')}]` : 'backlinks: []',
week ? `week: ${week.monday} ${week.sunday}` : '',
`modified: ${new Date().toISOString()}`,
'---',
].filter(Boolean);
return lines.join('\n');
}
private applyHeader(content: string, header: string): string {
return `${header}\n\n${this.stripHeader(content)}`;
}
private updateFrontmatter(content: string, updates: {links?: string[], backlinks?: string[]}): string {
const match = content.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
if (!match) return content;
const [, fm, body] = match;
let newFm = fm;
if (updates.links !== undefined) {
const linksList = updates.links.length ? `[${updates.links.map(l => `"${l}"`).join(', ')}]` : '[]';
newFm = newFm.replace(/^links:.*$/m, `links: ${linksList}`);
}
if (updates.backlinks !== undefined) {
const backlinksList = updates.backlinks.length ? `[${updates.backlinks.map(l => `"${l}"`).join(', ')}]` : '[]';
newFm = newFm.replace(/^backlinks:.*$/m, `backlinks: ${backlinksList}`);
}
newFm = newFm.replace(/^modified:.*$/m, `modified: ${new Date().toISOString()}`);
return `---\n${newFm}\n---\n\n${body}`;
}
private stripHeader(content: string): string {
return content.replace(/^---[\s\S]*?\n---\n?/, '').trimStart();
}
private async docAgent(node: Memory, facts: string[], memories: Memory[], options: LLMRequest, tempName: string, week: {monday: string, sunday: string} | undefined, entry: {request: {abort?: () => void} | null}): Promise<boolean> {
const {links: oldLinks} = extractMetadata(node.content);
const currentBody = this.stripHeader(node.content);
let update;
try {
for(let i = 0; i < 3 && !update?.content; i++) {
const request = this.llm.ask(`New Facts:\n${facts.map(f => `- ${f}`).join('\n')}`, {
model: options.model,
temperature: 0.3,
system: `You are a knowledge base editor. Integrate the provided facts into the document below.
schema: {
description: {type: 'string', description: 'One-line description of what this document covers, no formatting or emojis', required: true},
content: {type: 'string', description: 'Rewritten document in markdown, without the frontmatter block', required: true},
},
system: `You are a knowledge base editor. Rewrite the current document below so it incorporates the new facts.
Formatting rules:
- Use Obsidian-style markdown: # headings, **bold** for key terms, bullet lists for facts
- Link related concepts with [[WikiLink]] notation — only link things that are genuinely related
- You may create links to nodes that don't exist yet if the concept is important
- Use Obsidian-style markdown: # headings, **bold** to add emphasis, __italics__ for titles, terms, etc, bullet & numbered lists for grouped 1D data and tables for 2D data
- Link related concepts with [[WikiLink]] notation using full paths like [[People/Sarah]] or [[Projects/Website]]
- Create links for specific entities (person, place, project, program) and abstract concepts (quantum mechanics, entropy) but skip generics (car, red, dog)
- Keep the document concise, factual, and human-readable
- Resolve any contradictions between old content and new facts (new facts win)
- Do not add filler, preamble, or AI commentary — just clean knowledge documents
- Resolve contradictions: the new facts always win — delete the outdated statement entirely, never keep both
- Later facts in the list override earlier ones
- Do not add frontmatter blocks, filler, preamble, or AI commentary
${week ? '- This is a weekly journal entry.\n' : ''}
All nodes:
${this.listNodes(memories).map(n => n.name).join(', ') || 'none'}
Current document:
\`\`\`markdown
${node.content || '(empty — this is a new document)'}
\`\`\``,
tools: [{
name: 'update_document',
description: 'Write the complete updated document content',
args: {
description: {type: 'string', description: 'One-line description of what this document covers, no formatting or emojis', required: true},
content: {type: 'string', description: 'Fully updated document in markdown', required: true},
},
fn:(args: any) => {
node.description = args.description;
finalContent = args.content;
return 'Saved';
}
}]
}
${currentBody}
\`\`\``}
);
entry.request = request;
update = await request;
}
} catch (err: any) {
if (err?.name === 'AbortError') return false;
throw err;
} finally {
entry.request = null;
}
node.content = finalContent;
node.links = extractLinks(finalContent);
const needsEmbed = !node.embedding?.length || node.description !== memories.find(m => m.name === node.name)?.description;
if (needsEmbed) {
const [e] = await this.llm.embedding(node.description);
if(!update?.content) return false;
const newLinks = extractLinks(update.content).filter(l => l !== node.name && l !== tempName);
const newLinkSet = new Set(newLinks);
const oldLinkSet = new Set(oldLinks);
for (const added of newLinkSet) {
if (!oldLinkSet.has(added)) {
const target = memories.find(m => m.name === added);
if (target) {
const {backlinks} = extractMetadata(target.content);
if (!backlinks.includes(node.name)) {
target.content = this.updateFrontmatter(target.content, {
backlinks: [...backlinks, node.name],
});
}
}
}
}
for (const removed of oldLinkSet) {
if (!newLinkSet.has(removed)) {
const target = memories.find(m => m.name === removed);
if (target) {
const {backlinks} = extractMetadata(target.content);
target.content = this.updateFrontmatter(target.content, {
backlinks: backlinks.filter(b => b !== node.name),
});
}
}
}
const {backlinks} = extractMetadata(node.content);
node.description = update.description;
node.content = this.applyHeader(update.content, this.buildHeader(node, week, newLinks, backlinks));
const [e] = await this.llm.embedding(node.content);
if(e) node.embedding = e.embedding;
}
return true;
}
private async factAgent(conversation: string, memories: Memory[], options: LLMRequest): Promise<FactBucket[]> {
const buckets: FactBucket[] = [];
private async factAgent(conversation: string, memories: Memory[], options: LLMRequest, weekKey: string): Promise<FactBucket[]> {
const buckets = new Map<string, string[]>();
await this.llm.ask(conversation, {
model: options.model,
temperature: 0.2,
system: `You are a fact extractor. Analyze this conversation and extract facts worth remembering long-term.
Rules:
- ONLY extract facts the USER explicitly stated about themselves, their work, or their projects
- ONLY extract current facts the USER explicitly stated about themselves, their work, or their projects
- ONLY extract decisions that were MADE during this conversation
- DO NOT extract anything the AI said, its capabilities, or meta-conversation about the AI
- DO NOT extract greetings, pleasantries, or generic exchanges
- DO NOT extract deltas or changes in facts; ONLY the end fact
- If nothing worth remembering was said, do not call any tools
Group facts by subject. For each group call \`extract_facts\` once.
Known nodes (name: description):
${this.listNodes(memories).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}`,
tools: [{
name: 'extract_facts',
description: 'Submit a group of related facts for a specific subject',
args: {
subject: {type: 'string', description: 'Subject matter facts regard', required: true},
facts: {type: 'string', description: 'Comma-separated list of extracted facts', required: true},
},
fn: (args: any) => {
buckets.push({
subject: args.subject,
facts: args.facts.split(',').map((f: string) => f.trim()).filter(Boolean),
});
return 'Recorded';
}
}]
});
return buckets;
}
private async organizingAgent(bucket: FactBucket, memories: Memory[], options: LLMRequest): Promise<Memory> {
let candidates = this.listNodes(memories);
let attempts = 0;
const maxAttempts = 3;
while (attempts++ < maxAttempts) {
let home = '', mode: string | null = null;
const resp = await this.llm.ask(`Subject: ${bucket.subject}\n\nFacts:\n${bucket.facts.map(f => `- ${f}`).join('\n')}`, {
model: options.model,
temperature: 0.1,
system: `You are a knowledge organizer. Your job is to find the correct home for the supplied facts.
1. Review the facts and the node list below. Pick the most likely match or decide if a new node is needed.
2. If you picked an existing node, use \`read\` to verify it's the right place.
- After reading, call either \`confirm\` (correct node) or \`mismatched\` (wrong node).
3. If none of the nodes match, call \`create\` to make a new node.
When extracting facts, you MUST also decide the exact destination path:
- Use an existing node name if the facts clearly belong there
- All information primary about the user should go under "Personal/..." (e.g., Personal/Info, Personal/Todos)
- When required, create a new path following collection/subject format (e.g., People/Sarah, Projects/Oxide)
- For journal entries, use "Journal"
Available nodes:
${candidates.map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None — create a new node.'}`,
- Journal
${this.listNodes(memories).filter(n => !n.name.includes('_temp_') && !n.name.includes('Journal')).map(n => `- ${n.name}: ${n.description}`).join('\n') || 'None yet.'}`,
tools: [{
name: 'read',
description: 'Read a node file to verify it is the right home for these facts',
args: {name: {type: 'string', description: 'Exact node name', required: true}},
fn: ({name}) => {
const mem = memories.find(m => m.name === name);
if (!mem) return 'Node not found';
home = name;
return this.formatMemory(mem);
}
}, {
name: 'confirm',
description: 'Confirm this is the correct node for the facts',
args: {},
fn: () => {
mode = 'success';
resp.abort();
}
}, {
name: 'mismatched',
description: 'This is not the node you are looking for',
args: {},
fn: () => {
mode = 'failed';
resp.abort();
}
}, {
name: 'create',
description: 'No existing node fits — create a new one',
args: {name: {type: 'string', description: 'Canonical name for the new node', required: true}},
fn: ({name}) => {
home = name;
mode = 'create';
resp.abort();
}
}]
name: 'facts_extract',
description: 'Submit facts with their destination',
args: {
destination: {type: 'string', description: 'Exact existing node name OR new path (e.g. "People/Sarah", "Projects/Oxide")', required: true},
facts: {type: 'string', description: 'Comma-separated facts', required: true},
},
fn: (args: any) => {
const subject = args.destination.trim().toLowerCase() === 'journal'
? `Journal/${weekKey}`
: args.destination.trim();
const facts = buckets.get(subject) ?? [];
facts.push(...dedupeFacts(String(args.facts).split(',')));
buckets.set(subject, facts);
return 'Recorded';
},
}],
});
if(mode === 'create') {
return this.createNode(home, memories);
} else if (mode === 'failed') {
candidates = candidates.filter(c => c.name !== home);
if(!candidates.length) return this.createNode(bucket.subject, memories);
} else if (mode === 'success') {
const existing = memories.find(m => m.name === home);
return existing || this.createNode(home, memories);
}
}
return this.createNode(bucket.subject, memories);
return buckets.entries().toArray().map(([subject, facts]) => ({subject, facts}));
}
}

View File

@@ -1,5 +1,5 @@
import {AbortablePromise} from './ai.ts';
import {LLMMessage, LLMRequest} from './llm.ts';
import {LLMRequest} from './llm.ts';
export abstract class LLMProvider {
abstract ask(message: string, options: LLMRequest): AbortablePromise<string>;

View File

@@ -100,16 +100,11 @@ export const CliTool: AiTool = {
export const DateTimeTool: AiTool = {
name: 'get_datetime',
description: 'Get local date / time',
args: {},
fn: async () => new Date().toString()
}
export const DateTimeUTCTool: AiTool = {
name: 'get_datetime_utc',
description: 'Get current UTC date / time',
args: {},
fn: async () => new Date().toUTCString()
description: 'Get local/UTC date/time',
args: {
timezone: {type: 'string', description: 'Which timezone to return, defaults to local', enum: ['local', 'utc'], default: 'local'}
},
fn: ({timezone}) => new Date()[timezone === 'local' ? 'toString' : 'toUTCString']()
}
export const ExecTool: AiTool = {
@@ -138,7 +133,7 @@ export const ExecTool: AiTool = {
}
export const FetchTool: AiTool = {
name: 'fetch',
name: 'net_fetch',
description: 'Make HTTP request to URL',
args: {
url: {type: 'string', description: 'URL to fetch', required: true},
@@ -168,7 +163,7 @@ export const JSTool: AiTool = {
}
export const PythonTool: AiTool = {
name: 'exec_javascript',
name: 'exec_python',
description: 'Execute commonjs javascript',
args: {
code: {type: 'string', description: 'CommonJS javascript', required: true}
@@ -177,7 +172,7 @@ export const PythonTool: AiTool = {
}
export const ReadWebpageTool: AiTool = {
name: 'read_webpage',
name: 'net_read',
description: 'Extract clean content from webpages, or convert media/documents to accessible formats',
args: {
url: {type: 'string', description: 'URL to read', required: true},
@@ -281,7 +276,7 @@ export const ReadWebpageTool: AiTool = {
};
export const WebSearchTool: AiTool = {
name: 'web_search',
name: 'net_search',
description: 'Use duckduckgo (anonymous) to find find relevant online resources. Returns a list of URLs that works great with the `read_webpage` tool',
args: {
query: {type: 'string', description: 'Search string', required: true},
@@ -307,7 +302,7 @@ export const WebSearchTool: AiTool = {
}
export const WikipediaTool: AiTool = {
name: 'wikipedia_search',
name: 'get_wikipedia',
description: 'Search Wikipedia for matching articles',
args: {
query: {type: 'string', description: 'Search term or article title', required: true},