feat: replace identitydb with supermemory
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
DB_PATH=./brainbox.db
|
SUPERMEMORY_API_KEY=
|
||||||
|
|
||||||
BRAINDB_PATH=./braindb.json
|
BRAINDB_PATH=./braindb.json
|
||||||
|
|
||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
|
|||||||
58
bun.lock
58
bun.lock
@@ -9,9 +9,9 @@
|
|||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"commander": "^15.0.0",
|
"commander": "^15.0.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"identitydb": "^0.5.2",
|
|
||||||
"ora": "^9.4.0",
|
"ora": "^9.4.0",
|
||||||
"prettier": "^3.8.3",
|
"prettier": "^3.8.3",
|
||||||
|
"supermemory": "^4.24.12",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
@@ -31,8 +31,6 @@
|
|||||||
|
|
||||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
|
|
||||||
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
@@ -43,90 +41,40 @@
|
|||||||
|
|
||||||
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
"commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||||
|
|
||||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
|
||||||
|
|
||||||
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||||
|
|
||||||
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
|
||||||
|
|
||||||
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
|
||||||
|
|
||||||
"identitydb": ["identitydb@0.5.2", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-AkUmmAvpkgtIiHi7l1cTYFQDBYDMSi9HG94IAPWAq8Qa3wHbwaUaDCbPgxJM1ypN63SiW8vBllRTa28W3JXa3Q=="],
|
|
||||||
|
|
||||||
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
|
||||||
|
|
||||||
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
|
||||||
|
|
||||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||||
|
|
||||||
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
|
|
||||||
|
|
||||||
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
"log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||||
|
|
||||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
|
||||||
|
|
||||||
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
|
|
||||||
|
|
||||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
"mysql2": ["mysql2@3.22.4", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-CtXYlmL7ZamiYKbmqkamQHWJROUHSfm+f3kByzGfknw7kW51mcB2ouMUqYq1XfYxbXmnWo6RhPydx6OCqdgcmQ=="],
|
|
||||||
|
|
||||||
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
|
|
||||||
|
|
||||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
"ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="],
|
"ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="],
|
||||||
|
|
||||||
"pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
|
|
||||||
|
|
||||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
|
||||||
|
|
||||||
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
|
|
||||||
|
|
||||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
|
||||||
|
|
||||||
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
|
||||||
|
|
||||||
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
|
|
||||||
|
|
||||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
|
||||||
|
|
||||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
|
||||||
|
|
||||||
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
|
||||||
|
|
||||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
|
||||||
|
|
||||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
|
||||||
|
|
||||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
|
||||||
|
|
||||||
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
"restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||||
|
|
||||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
|
||||||
|
|
||||||
"sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
|
|
||||||
|
|
||||||
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
|
"stdin-discarder": ["stdin-discarder@0.3.2", "", {}, "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A=="],
|
||||||
|
|
||||||
"string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
"string-width": ["string-width@8.2.1", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
|
||||||
|
|
||||||
|
"supermemory": ["supermemory@4.24.12", "", { "bin": { "supermemory": "bin/cli" } }, "sha512-xAFextuqk4JuoW33jJaFGqT1oMppN2IgfWUrV18Fv3qAAZ6M1SR1tb+7EBq8vrEQIx4iY2MQh5p+qnfL6lI8Yw=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||||
|
|
||||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
|
||||||
|
|
||||||
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
"yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
|
||||||
|
|
||||||
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
|
||||||
|
|||||||
@@ -23,8 +23,8 @@
|
|||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"commander": "^15.0.0",
|
"commander": "^15.0.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"identitydb": "^0.5.2",
|
|
||||||
"ora": "^9.4.0",
|
"ora": "^9.4.0",
|
||||||
"prettier": "^3.8.3"
|
"prettier": "^3.8.3",
|
||||||
|
"supermemory": "^4.24.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ You will be given:
|
|||||||
5. ALWAYS reply in real time. The user expects a person typing back, not a polished essay.
|
5. ALWAYS reply in real time. The user expects a person typing back, not a polished essay.
|
||||||
6. ALWAYS filter every response through the persona's voice, vocabulary, and emotional weather.
|
6. ALWAYS filter every response through the persona's voice, vocabulary, and emotional weather.
|
||||||
7. ALWAYS stay consistent with the date, time, and schedules you were given. Do not contradict them.
|
7. ALWAYS stay consistent with the date, time, and schedules you were given. Do not contradict them.
|
||||||
8. ALWAYS remember what you already know about the user. Do not ask for facts you already have; use the `searchIdentityDB` tool to look them up.
|
8. ALWAYS remember what you already know about the user. Do not ask for facts you already have; use the `searchMemory` tool to look them up.
|
||||||
|
|
||||||
### HOW TO REPLY
|
### HOW TO REPLY
|
||||||
|
|
||||||
@@ -29,8 +29,8 @@ You will be given:
|
|||||||
### WHEN TO USE TOOLS
|
### WHEN TO USE TOOLS
|
||||||
|
|
||||||
- Call `addReplyMessage` for every bubble you want to send. When you have no more to say, end your turn (do not call any tool, return plain text).
|
- Call `addReplyMessage` for every bubble you want to send. When you have no more to say, end your turn (do not call any tool, return plain text).
|
||||||
- Call `searchIdentityDB` whenever the user references something you might already know but you cannot recall precisely. Use the natural-language query that would best match the relevant fact.
|
- Call `searchMemory` whenever the user references something you might already know but you cannot recall precisely. Use the natural-language query that would best match the relevant fact.
|
||||||
- Do not call `searchIdentityDB` for greetings, small talk, or anything you can answer from the persona's own knowledge of the user.
|
- Do not call `searchMemory` for greetings, small talk, or anything you can answer from the persona's own knowledge of the user.
|
||||||
|
|
||||||
### FINAL MANDATE
|
### FINAL MANDATE
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { llm } from "@/openrouter";
|
|
||||||
import { extractedFactSchema, type ExtractedFactResult } from "@/openrouter/schema";
|
|
||||||
import { LlmFactExtractor } from "identitydb";
|
|
||||||
|
|
||||||
export const factExtractor = new LlmFactExtractor({
|
|
||||||
model: {
|
|
||||||
async generateText({ instruction, input }) {
|
|
||||||
const result = await llm.call<ExtractedFactResult>(llm.models.identity, {
|
|
||||||
instruction,
|
|
||||||
message: input,
|
|
||||||
jsonSchemaName: "fact-extractor",
|
|
||||||
jsonSchema: extractedFactSchema,
|
|
||||||
});
|
|
||||||
return result.items;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
test,
|
test,
|
||||||
} from "bun:test";
|
} from "bun:test";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import { IdentityDB, type Space } from "identitydb";
|
import type { Space } from "./types";
|
||||||
|
|
||||||
const llmCalls: Array<{ model: unknown; options: any }> = [];
|
const llmCalls: Array<{ model: unknown; options: any }> = [];
|
||||||
let customMonthlyDays: Array<{ day: number; summary: string }> | null = null;
|
let customMonthlyDays: Array<{ day: number; summary: string }> | null = null;
|
||||||
@@ -24,11 +24,6 @@ let customAvailability: Array<{
|
|||||||
status: string;
|
status: string;
|
||||||
}> | null = null;
|
}> | null = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue of LLM responses for tool-calling flows (sendMessage). Each entry is
|
|
||||||
* returned in order. Shape matches OpenRouter's `ChatResult.choices[0]`
|
|
||||||
* reduced form: `{ content, tool_calls, finish_reason }`.
|
|
||||||
*/
|
|
||||||
type ToolCallResponse = {
|
type ToolCallResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -136,6 +131,9 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
|
|||||||
},
|
},
|
||||||
} as unknown as T;
|
} as unknown as T;
|
||||||
}
|
}
|
||||||
|
if (typeof options.message === "string" || options.message === undefined) {
|
||||||
|
return "test-description" as unknown as T;
|
||||||
|
}
|
||||||
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
|
throw new Error(`unexpected jsonSchemaName: ${options.jsonSchemaName}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,14 +148,191 @@ mock.module("@/openrouter", () => ({
|
|||||||
mock.module("@/config", () => ({
|
mock.module("@/config", () => ({
|
||||||
config: {
|
config: {
|
||||||
openrouterApiKey: "test-key",
|
openrouterApiKey: "test-key",
|
||||||
dbPath: ":memory:",
|
supermemoryApiKey: "test-supermemory-key",
|
||||||
braindbPath: "/tmp/brainbox-test-braindb.json",
|
braindbPath: "/tmp/brainbox-test-braindb.json",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
interface StoredDoc {
|
||||||
|
id: string;
|
||||||
|
customId: string | null;
|
||||||
|
containerTag: string;
|
||||||
|
content: string;
|
||||||
|
summary: string | null;
|
||||||
|
metadata: Record<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockSupermemory {
|
||||||
|
docs = new Map<string, StoredDoc>();
|
||||||
|
private nextId = 0;
|
||||||
|
documentsAddCalls = 0;
|
||||||
|
|
||||||
|
constructor(_options: { apiKey: string }) {}
|
||||||
|
|
||||||
|
documents = {
|
||||||
|
add: async (params: {
|
||||||
|
content: string;
|
||||||
|
containerTag: string;
|
||||||
|
customId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
|
this.documentsAddCalls += 1;
|
||||||
|
const id = `mock-${++this.nextId}`;
|
||||||
|
const stored: StoredDoc = {
|
||||||
|
id,
|
||||||
|
customId: params.customId ?? null,
|
||||||
|
containerTag: params.containerTag,
|
||||||
|
content: params.content,
|
||||||
|
summary: null,
|
||||||
|
metadata: params.metadata ?? null,
|
||||||
|
};
|
||||||
|
this.docs.set(id, stored);
|
||||||
|
return { id, status: "done" };
|
||||||
|
},
|
||||||
|
list: async (params: {
|
||||||
|
containerTags?: Array<string>;
|
||||||
|
limit?: number;
|
||||||
|
}) => {
|
||||||
|
const tags = params.containerTags ?? [];
|
||||||
|
const limit = params.limit ?? 200;
|
||||||
|
const all = Array.from(this.docs.values()).filter((d) =>
|
||||||
|
tags.length === 0 ? true : tags.includes(d.containerTag),
|
||||||
|
);
|
||||||
|
const memories = all.slice(0, limit).map((d) => ({
|
||||||
|
id: d.id,
|
||||||
|
customId: d.customId,
|
||||||
|
containerTag: d.containerTag,
|
||||||
|
summary: d.summary,
|
||||||
|
metadata: d.metadata as
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Record<string, unknown>
|
||||||
|
| Array<unknown>
|
||||||
|
| null,
|
||||||
|
content: d.content,
|
||||||
|
createdAt: "2026-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
status: "done" as const,
|
||||||
|
type: "text" as const,
|
||||||
|
connectionId: null,
|
||||||
|
filepath: null,
|
||||||
|
title: null,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
memories,
|
||||||
|
pagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
totalItems: memories.length,
|
||||||
|
totalPages: 1,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
get: async (id: string) => {
|
||||||
|
const d = this.docs.get(id);
|
||||||
|
if (!d) {
|
||||||
|
throw new Error(`MockSupermemory.documents.get: no such id ${id}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
customId: d.customId,
|
||||||
|
containerTag: d.containerTag,
|
||||||
|
content: d.content,
|
||||||
|
summary: d.summary,
|
||||||
|
metadata: d.metadata as
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| Record<string, unknown>
|
||||||
|
| Array<unknown>
|
||||||
|
| null,
|
||||||
|
createdAt: "2026-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
status: "done" as const,
|
||||||
|
type: "text" as const,
|
||||||
|
connectionId: null,
|
||||||
|
filepath: null,
|
||||||
|
title: null,
|
||||||
|
source: null,
|
||||||
|
ogImage: null,
|
||||||
|
raw: null,
|
||||||
|
spatialPoint: null,
|
||||||
|
taskType: "memory" as const,
|
||||||
|
url: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
search = {
|
||||||
|
execute: async (params: {
|
||||||
|
q: string;
|
||||||
|
containerTag?: string;
|
||||||
|
limit?: number;
|
||||||
|
onlyMatchingChunks?: boolean;
|
||||||
|
}) => {
|
||||||
|
const q = params.q.toLowerCase();
|
||||||
|
const limit = params.limit ?? 5;
|
||||||
|
const hits = Array.from(this.docs.values())
|
||||||
|
.filter(
|
||||||
|
(d) =>
|
||||||
|
(params.containerTag
|
||||||
|
? d.containerTag === params.containerTag
|
||||||
|
: true) && d.content.toLowerCase().includes(q),
|
||||||
|
)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((d, i) => ({
|
||||||
|
chunks: [
|
||||||
|
{
|
||||||
|
content: d.content,
|
||||||
|
isRelevant: true,
|
||||||
|
score: 1 - i * 0.1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
summary: d.summary,
|
||||||
|
score: 1 - i * 0.1,
|
||||||
|
documentId: d.id,
|
||||||
|
metadata: (d.metadata as Record<string, unknown>) ?? null,
|
||||||
|
createdAt: "2026-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
title: d.customId,
|
||||||
|
type: "text" as const,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
results: hits,
|
||||||
|
total: hits.length,
|
||||||
|
timing: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
findByCustomId(customId: string): StoredDoc | undefined {
|
||||||
|
for (const d of this.docs.values()) {
|
||||||
|
if (d.customId === customId) return d;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
this.docs.clear();
|
||||||
|
this.nextId = 0;
|
||||||
|
this.documentsAddCalls = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the real `supermemory` SDK with our in-memory mock. The
|
||||||
|
* static factories `Brain.create` / `Brain.createDebug` / `Brain.load`
|
||||||
|
* all do `new Supermemory({ apiKey })` internally; this mock is what
|
||||||
|
* they pick up.
|
||||||
|
*/
|
||||||
|
mock.module("supermemory", () => ({
|
||||||
|
default: MockSupermemory,
|
||||||
|
}));
|
||||||
|
|
||||||
const { Brain } = await import("./index");
|
const { Brain } = await import("./index");
|
||||||
const { brainManager } = await import("./manager");
|
const { brainManager } = await import("./manager");
|
||||||
const { formatDateKey, nextDay, nextMonth } = await import("./schedule");
|
const { formatDateKey, nextMonth } = await import("./schedule");
|
||||||
type BrainItem = import("./manager").BrainItem;
|
type BrainItem = import("./manager").BrainItem;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -168,16 +343,10 @@ beforeAll(async () => {
|
|||||||
|
|
||||||
afterAll(async () => {});
|
afterAll(async () => {});
|
||||||
|
|
||||||
async function makeBrain(
|
async function makeBrain(): Promise<InstanceType<typeof Brain>> {
|
||||||
embeddingProvider: unknown = NOOP_EMBEDDING_PROVIDER,
|
const db = new MockSupermemory({ apiKey: "test-supermemory-key" });
|
||||||
): Promise<InstanceType<typeof Brain>> {
|
|
||||||
const db = await IdentityDB.connect({
|
|
||||||
client: "sqlite",
|
|
||||||
filename: ":memory:",
|
|
||||||
});
|
|
||||||
await db.initialize();
|
|
||||||
const spaceName = `test-space-${randomUUID()}`;
|
const spaceName = `test-space-${randomUUID()}`;
|
||||||
const space: Space = await db.upsertSpace({ name: spaceName });
|
const space: Space = { name: spaceName, description: "Test Brain space" };
|
||||||
const brainbase: BrainItem = {
|
const brainbase: BrainItem = {
|
||||||
brainId: randomUUID(),
|
brainId: randomUUID(),
|
||||||
spaceName,
|
spaceName,
|
||||||
@@ -185,7 +354,7 @@ async function makeBrain(
|
|||||||
baseSystemPrompt:
|
baseSystemPrompt:
|
||||||
"Test personality: night owl, introverted, studies at midnight.",
|
"Test personality: night owl, introverted, studies at midnight.",
|
||||||
};
|
};
|
||||||
return new Brain(db, space, brainbase, false, embeddingProvider as never);
|
return new Brain(db as never, space, brainbase, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -197,10 +366,11 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Brain.createDailySchedule", () => {
|
describe("Brain.createDailySchedule", () => {
|
||||||
test("S1: returns 48 slots in 30-min intervals and persists a fact", async () => {
|
test("S1: returns 48 slots in 30-min intervals and persists a document", async () => {
|
||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 5, 5);
|
const today = new Date(2026, 5, 5);
|
||||||
const expectedTomorrow = nextDay(today);
|
const expectedTomorrow = (await import("./schedule")).nextDay(today);
|
||||||
const expectedKey = formatDateKey(expectedTomorrow);
|
const expectedKey = formatDateKey(expectedTomorrow);
|
||||||
|
|
||||||
const result = await brain.createDailySchedule(today, "focus on writing");
|
const result = await brain.createDailySchedule(today, "focus on writing");
|
||||||
@@ -228,50 +398,39 @@ describe("Brain.createDailySchedule", () => {
|
|||||||
expect(llmCall!.options.message).toContain("focus on writing");
|
expect(llmCall!.options.message).toContain("focus on writing");
|
||||||
expect(llmCall!.options.message).toContain("Test personality");
|
expect(llmCall!.options.message).toContain("Test personality");
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
|
||||||
`daily-schedule:${expectedKey}`,
|
expect(stored).toBeDefined();
|
||||||
{
|
expect(stored!.containerTag).toBe(brain.space.name);
|
||||||
spaceName: brain.space.name,
|
expect(JSON.parse(stored!.content).items).toHaveLength(48);
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(facts).toHaveLength(1);
|
|
||||||
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(48);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("S4: month wrap (June 30 -> July 1)", async () => {
|
test("S4: month wrap (June 30 -> July 1)", async () => {
|
||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 5, 30);
|
const today = new Date(2026, 5, 30);
|
||||||
const expectedKey = formatDateKey(new Date(2026, 6, 1));
|
const expectedKey = formatDateKey(new Date(2026, 6, 1));
|
||||||
|
|
||||||
await brain.createDailySchedule(today, "");
|
await brain.createDailySchedule(today, "");
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
|
||||||
`daily-schedule:${expectedKey}`,
|
expect(stored).toBeDefined();
|
||||||
{
|
|
||||||
spaceName: brain.space.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(facts).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("S4b: year wrap (December 31 -> January 1 next year)", async () => {
|
test("S4b: year wrap (December 31 -> January 1 next year)", async () => {
|
||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 11, 31);
|
const today = new Date(2026, 11, 31);
|
||||||
const expectedKey = "2027-01-01";
|
const expectedKey = "2027-01-01";
|
||||||
|
|
||||||
await brain.createDailySchedule(today, "");
|
await brain.createDailySchedule(today, "");
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const stored = db.findByCustomId(`daily-schedule:${expectedKey}`);
|
||||||
`daily-schedule:${expectedKey}`,
|
expect(stored).toBeDefined();
|
||||||
{
|
|
||||||
spaceName: brain.space.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(facts).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("S6: consumes monthly summary for the target day when present", async () => {
|
test("S6: consumes monthly summary for the target day when present", async () => {
|
||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
|
|
||||||
customMonthlyDays = Array.from({ length: 30 }, (_, i) => ({
|
customMonthlyDays = Array.from({ length: 30 }, (_, i) => ({
|
||||||
day: i + 1,
|
day: i + 1,
|
||||||
@@ -282,13 +441,8 @@ describe("Brain.createDailySchedule", () => {
|
|||||||
const todayForMonthly = new Date(2026, 4, 15);
|
const todayForMonthly = new Date(2026, 4, 15);
|
||||||
await brain.createMonthlySchedule(todayForMonthly, "");
|
await brain.createMonthlySchedule(todayForMonthly, "");
|
||||||
|
|
||||||
const monthlyFacts = await brain.db.getTopicFacts(
|
const monthlyStored = db.findByCustomId("monthly-schedule:2026-06");
|
||||||
`monthly-schedule:2026-06`,
|
expect(monthlyStored).toBeDefined();
|
||||||
{
|
|
||||||
spaceName: brain.space.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(monthlyFacts).toHaveLength(1);
|
|
||||||
|
|
||||||
llmCalls.length = 0;
|
llmCalls.length = 0;
|
||||||
customDailySlots = build48Slots();
|
customDailySlots = build48Slots();
|
||||||
@@ -304,11 +458,63 @@ describe("Brain.createDailySchedule", () => {
|
|||||||
"UNIQUE_SUMMARY_FOR_DAY_10",
|
"UNIQUE_SUMMARY_FOR_DAY_10",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("S9: injects 2-days-ago schedule as recent context when one exists", async () => {
|
||||||
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
|
|
||||||
|
const twoDaysAgoTarget = new Date(2026, 5, 7);
|
||||||
|
const twoDaysAgoTomorrow = (await import("./schedule")).nextDay(
|
||||||
|
twoDaysAgoTarget,
|
||||||
|
);
|
||||||
|
const twoDaysAgoKey = formatDateKey(twoDaysAgoTomorrow);
|
||||||
|
|
||||||
|
await brain.add({
|
||||||
|
customId: `daily-schedule:${twoDaysAgoKey}`,
|
||||||
|
content: JSON.stringify({
|
||||||
|
items: Array.from({ length: 48 }, (_, i) => ({
|
||||||
|
start: `${String(Math.floor(i / 2)).padStart(2, "0")}:${String((i % 2) * 30).padStart(2, "0")}`,
|
||||||
|
end: `${String(Math.floor((i + 1) / 2)).padStart(2, "0")}:${String(((i + 1) % 2) * 30).padStart(2, "0")}`,
|
||||||
|
activity: `prior-day-activity-${i}`,
|
||||||
|
notes: "",
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
metadata: { kind: "schedule", source: "createDailySchedule", date: twoDaysAgoKey },
|
||||||
|
});
|
||||||
|
|
||||||
|
llmCalls.length = 0;
|
||||||
|
const today = new Date(2026, 5, 9);
|
||||||
|
await brain.createDailySchedule(today, "");
|
||||||
|
|
||||||
|
const dailyLlmCall = llmCalls.find(
|
||||||
|
(c) => c.options.jsonSchemaName === "daily-schedule",
|
||||||
|
);
|
||||||
|
expect(dailyLlmCall).toBeDefined();
|
||||||
|
expect(dailyLlmCall!.options.message).toContain(
|
||||||
|
`Recent schedule (${twoDaysAgoKey}, 2 days ago):`,
|
||||||
|
);
|
||||||
|
expect(dailyLlmCall!.options.message).toContain("prior-day-activity-0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("S10: 2-days-ago context says 'no schedule on file' when prior day is missing", async () => {
|
||||||
|
const brain = await makeBrain();
|
||||||
|
const today = new Date(2026, 5, 9);
|
||||||
|
await brain.createDailySchedule(today, "");
|
||||||
|
|
||||||
|
const dailyLlmCall = llmCalls.find(
|
||||||
|
(c) => c.options.jsonSchemaName === "daily-schedule",
|
||||||
|
);
|
||||||
|
expect(dailyLlmCall).toBeDefined();
|
||||||
|
expect(dailyLlmCall!.options.message).toContain(
|
||||||
|
"(no schedule on file for 2 days ago)",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Brain.createMonthlySchedule", () => {
|
describe("Brain.createMonthlySchedule", () => {
|
||||||
test("S2: returns N day summaries (N = days in next month) and persists a fact", async () => {
|
test("S2: returns N day summaries (N = days in next month) and persists a document", async () => {
|
||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 0, 15);
|
const today = new Date(2026, 0, 15);
|
||||||
const expected = nextMonth(today);
|
const expected = nextMonth(today);
|
||||||
const expectedKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
|
const expectedKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
|
||||||
@@ -329,20 +535,16 @@ describe("Brain.createMonthlySchedule", () => {
|
|||||||
expect(llmCall!.options.message).toContain("study for GRE");
|
expect(llmCall!.options.message).toContain("study for GRE");
|
||||||
expect(llmCall!.options.message).toContain("Test personality");
|
expect(llmCall!.options.message).toContain("Test personality");
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`);
|
||||||
`monthly-schedule:${expectedKey}`,
|
expect(stored).toBeDefined();
|
||||||
{
|
expect(JSON.parse(stored!.content).items).toHaveLength(
|
||||||
spaceName: brain.space.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(facts).toHaveLength(1);
|
|
||||||
expect(JSON.parse(facts[0]!.statement).items).toHaveLength(
|
|
||||||
expected.daysInMonth,
|
expected.daysInMonth,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("S5: year wrap (December 15 -> January next year)", async () => {
|
test("S5: year wrap (December 15 -> January next year)", async () => {
|
||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 11, 15);
|
const today = new Date(2026, 11, 15);
|
||||||
const expectedKey = "2027-01";
|
const expectedKey = "2027-01";
|
||||||
|
|
||||||
@@ -351,13 +553,8 @@ describe("Brain.createMonthlySchedule", () => {
|
|||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result!.items).toHaveLength(31);
|
expect(result!.items).toHaveLength(31);
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const stored = db.findByCustomId(`monthly-schedule:${expectedKey}`);
|
||||||
`monthly-schedule:${expectedKey}`,
|
expect(stored).toBeDefined();
|
||||||
{
|
|
||||||
spaceName: brain.space.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(facts).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -366,25 +563,14 @@ describe("Brain.getTodayScheduledAvailability", () => {
|
|||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
const today = new Date(2026, 5, 10);
|
const today = new Date(2026, 5, 10);
|
||||||
const todayKey = formatDateKey(today);
|
const todayKey = formatDateKey(today);
|
||||||
await brain.db.addFact({
|
await brain.add({
|
||||||
spaceName: brain.space.name,
|
customId: `daily-schedule:${todayKey}`,
|
||||||
statement: JSON.stringify({ items: build48Slots() }),
|
content: JSON.stringify({ items: build48Slots() }),
|
||||||
summary: "test daily",
|
metadata: {
|
||||||
|
kind: "schedule",
|
||||||
source: "test",
|
source: "test",
|
||||||
confidence: 1.0,
|
date: todayKey,
|
||||||
topics: [
|
|
||||||
{
|
|
||||||
name: `daily-schedule:${todayKey}`,
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "daily-schedule",
|
|
||||||
category: "concept",
|
|
||||||
granularity: "abstract",
|
|
||||||
},
|
|
||||||
{ name: todayKey, category: "temporal", granularity: "concrete" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await brain.getTodayScheduledAvailability(today);
|
const result = await brain.getTodayScheduledAvailability(today);
|
||||||
@@ -417,25 +603,14 @@ describe("Brain.removeScheduledAvailability", () => {
|
|||||||
const brain = await makeBrain();
|
const brain = await makeBrain();
|
||||||
const today = new Date(2026, 5, 10);
|
const today = new Date(2026, 5, 10);
|
||||||
const todayKey = formatDateKey(today);
|
const todayKey = formatDateKey(today);
|
||||||
await brain.db.addFact({
|
await brain.add({
|
||||||
spaceName: brain.space.name,
|
customId: `daily-schedule:${todayKey}`,
|
||||||
statement: JSON.stringify({ items: build48Slots() }),
|
content: JSON.stringify({ items: build48Slots() }),
|
||||||
summary: "test daily",
|
metadata: {
|
||||||
|
kind: "schedule",
|
||||||
source: "test",
|
source: "test",
|
||||||
confidence: 1.0,
|
date: todayKey,
|
||||||
topics: [
|
|
||||||
{
|
|
||||||
name: `daily-schedule:${todayKey}`,
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "daily-schedule",
|
|
||||||
category: "concept",
|
|
||||||
granularity: "abstract",
|
|
||||||
},
|
|
||||||
{ name: todayKey, category: "temporal", granularity: "concrete" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const r1 = await brain.getTodayScheduledAvailability(today);
|
const r1 = await brain.getTodayScheduledAvailability(today);
|
||||||
@@ -463,25 +638,19 @@ describe("S8: regression on existing methods", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Brain.createDebug", () => {
|
describe("Brain.createDebug", () => {
|
||||||
test("D1: returns a Brain with debug=true, the supplied personality, and no disk file created", async () => {
|
test("D1: returns a Brain with debug=true and the supplied personality under the brain:debug namespace", async () => {
|
||||||
const { existsSync } = await import("fs");
|
|
||||||
const { resolve } = await import("path");
|
|
||||||
|
|
||||||
const before = existsSync(resolve(process.cwd(), "brainbox.db"));
|
|
||||||
|
|
||||||
const brain = await Brain.createDebug({ personality: "test-personality-Q" });
|
const brain = await Brain.createDebug({ personality: "test-personality-Q" });
|
||||||
|
|
||||||
expect(brain).toBeInstanceOf(Brain);
|
expect(brain).toBeInstanceOf(Brain);
|
||||||
expect(brain.debug).toBe(true);
|
expect(brain.debug).toBe(true);
|
||||||
expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q");
|
expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q");
|
||||||
expect(brain.brainbase.displayName).toBe("Debug Brain");
|
expect(brain.brainbase.displayName).toBe("Debug Brain");
|
||||||
|
expect(brain.space.name).toBe("brain:debug");
|
||||||
const after = existsSync(resolve(process.cwd(), "brainbox.db"));
|
|
||||||
expect(after).toBe(before);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("D2: createDailySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => {
|
test("D2: createDailySchedule on a debug brain returns a schedule and persists to brain:debug", async () => {
|
||||||
const brain = await Brain.createDebug({ personality: "p" });
|
const brain = await Brain.createDebug({ personality: "p" });
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 5, 5);
|
const today = new Date(2026, 5, 5);
|
||||||
const tomorrow = new Date(2026, 5, 6);
|
const tomorrow = new Date(2026, 5, 6);
|
||||||
const tomorrowKey = formatDateKey(tomorrow);
|
const tomorrowKey = formatDateKey(tomorrow);
|
||||||
@@ -490,14 +659,14 @@ describe("Brain.createDebug", () => {
|
|||||||
expect(schedule).not.toBeNull();
|
expect(schedule).not.toBeNull();
|
||||||
expect(schedule!.items).toHaveLength(48);
|
expect(schedule!.items).toHaveLength(48);
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(`daily-schedule:${tomorrowKey}`, {
|
const stored = db.findByCustomId(`daily-schedule:${tomorrowKey}`);
|
||||||
spaceName: brain.space.name,
|
expect(stored).toBeDefined();
|
||||||
});
|
expect(stored!.containerTag).toBe("brain:debug");
|
||||||
expect(facts).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("D3: createMonthlySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => {
|
test("D3: createMonthlySchedule on a debug brain returns a schedule and persists to brain:debug", async () => {
|
||||||
const brain = await Brain.createDebug({ personality: "p" });
|
const brain = await Brain.createDebug({ personality: "p" });
|
||||||
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const today = new Date(2026, 0, 15);
|
const today = new Date(2026, 0, 15);
|
||||||
const expected = nextMonth(today);
|
const expected = nextMonth(today);
|
||||||
const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
|
const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
|
||||||
@@ -506,42 +675,12 @@ describe("Brain.createDebug", () => {
|
|||||||
expect(schedule).not.toBeNull();
|
expect(schedule).not.toBeNull();
|
||||||
expect(schedule!.items).toHaveLength(expected.daysInMonth);
|
expect(schedule!.items).toHaveLength(expected.daysInMonth);
|
||||||
|
|
||||||
const facts = await brain.db.getTopicFacts(
|
const stored = db.findByCustomId(`monthly-schedule:${monthKey}`);
|
||||||
`monthly-schedule:${monthKey}`,
|
expect(stored).toBeDefined();
|
||||||
{ spaceName: brain.space.name },
|
expect(stored!.containerTag).toBe("brain:debug");
|
||||||
);
|
|
||||||
expect(facts).toHaveLength(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const NOOP_EMBEDDING_PROVIDER = {
|
|
||||||
model: "test-embed",
|
|
||||||
dimensions: 4,
|
|
||||||
async embed(_input: string): Promise<number[]> {
|
|
||||||
return [0, 0, 0, 0];
|
|
||||||
},
|
|
||||||
async embedMany(inputs: string[]): Promise<number[][]> {
|
|
||||||
return inputs.map(() => [0, 0, 0, 0]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCORING_EMBEDDING_PROVIDER = {
|
|
||||||
model: "test-embed-scoring",
|
|
||||||
dimensions: 4,
|
|
||||||
async embed(input: string): Promise<number[]> {
|
|
||||||
if (input.includes("coffee")) return [1, 0, 0, 0];
|
|
||||||
if (input.includes("pizza")) return [0, 1, 0, 0];
|
|
||||||
return [0, 0, 1, 0];
|
|
||||||
},
|
|
||||||
async embedMany(inputs: string[]): Promise<number[][]> {
|
|
||||||
return inputs.map((s) => {
|
|
||||||
if (s.includes("coffee")) return [1, 0, 0, 0];
|
|
||||||
if (s.includes("pizza")) return [0, 1, 0, 0];
|
|
||||||
return [0, 0, 1, 0];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("Brain.sendMessage — translateMessageHistory helper", () => {
|
describe("Brain.sendMessage — translateMessageHistory helper", () => {
|
||||||
test("SM1: translateMessageHistory produces the documented format with persona label and timestamps", async () => {
|
test("SM1: translateMessageHistory produces the documented format with persona label and timestamps", async () => {
|
||||||
const { translateMessageHistory } = await import("./messageHistory");
|
const { translateMessageHistory } = await import("./messageHistory");
|
||||||
@@ -600,7 +739,7 @@ describe("Brain.sendMessage — tool-calling flow", () => {
|
|||||||
}>
|
}>
|
||||||
).map((t) => t.function.name);
|
).map((t) => t.function.name);
|
||||||
expect(toolNames).toContain("addReplyMessage");
|
expect(toolNames).toContain("addReplyMessage");
|
||||||
expect(toolNames).toContain("searchIdentityDB");
|
expect(toolNames).toContain("searchMemory");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("SM4: sendMessage accumulates addReplyMessage tool calls and returns them in order", async () => {
|
test("SM4: sendMessage accumulates addReplyMessage tool calls and returns them in order", async () => {
|
||||||
@@ -635,20 +774,13 @@ describe("Brain.sendMessage — tool-calling flow", () => {
|
|||||||
expect(out).toEqual(["어.", "왜불러"]);
|
expect(out).toEqual(["어.", "왜불러"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("SM5: sendMessage feeds searchIdentityDB tool result back to the LLM", async () => {
|
test("SM5: sendMessage feeds searchMemory tool result back to the LLM", async () => {
|
||||||
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER);
|
const brain = await makeBrain();
|
||||||
const fact = await brain.db.addFact({
|
await brain.add({
|
||||||
spaceName: brain.space.name,
|
customId: "fact-coffee",
|
||||||
statement: "사용자는 커피를 좋아한다",
|
content: "사용자는 커피를 좋아한다",
|
||||||
summary: "user loves coffee",
|
metadata: { kind: "fact", source: "test" },
|
||||||
source: "test",
|
|
||||||
confidence: 1.0,
|
|
||||||
topics: [
|
|
||||||
{ name: "사용자", category: "entity", granularity: "concrete" },
|
|
||||||
{ name: "커피", category: "concept", granularity: "abstract" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await brain.indexFactEmbeddingFor(fact);
|
|
||||||
|
|
||||||
chatResponses = [
|
chatResponses = [
|
||||||
{
|
{
|
||||||
@@ -656,7 +788,7 @@ describe("Brain.sendMessage — tool-calling flow", () => {
|
|||||||
tool_calls: [
|
tool_calls: [
|
||||||
{
|
{
|
||||||
id: "call_s",
|
id: "call_s",
|
||||||
name: "searchIdentityDB",
|
name: "searchMemory",
|
||||||
arguments: JSON.stringify({ query: "커피" }),
|
arguments: JSON.stringify({ query: "커피" }),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -732,8 +864,8 @@ describe("Brain.sendMessage — tool-calling flow", () => {
|
|||||||
expect(userMsg!.content).toContain("하이");
|
expect(userMsg!.content).toContain("하이");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("SM7: createDailySchedule auto-indexes the new fact so it is searchable via the provider", async () => {
|
test("SM7: createDailySchedule persists a document reachable via brain.get", async () => {
|
||||||
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER);
|
const brain = await makeBrain();
|
||||||
const today = new Date(2026, 5, 5);
|
const today = new Date(2026, 5, 5);
|
||||||
const tomorrow = new Date(2026, 5, 6);
|
const tomorrow = new Date(2026, 5, 6);
|
||||||
const tomorrowKey = formatDateKey(tomorrow);
|
const tomorrowKey = formatDateKey(tomorrow);
|
||||||
@@ -741,43 +873,20 @@ describe("Brain.sendMessage — tool-calling flow", () => {
|
|||||||
customDailySlots = build48Slots();
|
customDailySlots = build48Slots();
|
||||||
await brain.createDailySchedule(today, "msg");
|
await brain.createDailySchedule(today, "msg");
|
||||||
|
|
||||||
const hits = await brain.db.searchFacts({
|
const stored = await brain.get(`daily-schedule:${tomorrowKey}`);
|
||||||
spaceName: brain.space.name,
|
expect(stored).not.toBeNull();
|
||||||
query: "slot-0",
|
expect(stored!.content).toContain("slot-0");
|
||||||
provider: SCORING_EMBEDDING_PROVIDER as never,
|
expect(stored!.metadata).toEqual({
|
||||||
limit: 5,
|
kind: "schedule",
|
||||||
|
source: "createDailySchedule",
|
||||||
|
date: tomorrowKey,
|
||||||
});
|
});
|
||||||
expect(hits.length).toBeGreaterThan(0);
|
|
||||||
const matched = hits.find((h) =>
|
|
||||||
h.statement.includes(`"activity":"slot-0"`),
|
|
||||||
);
|
|
||||||
expect(matched).toBeDefined();
|
|
||||||
|
|
||||||
const topicFacts = await brain.db.getTopicFacts(
|
|
||||||
`daily-schedule:${tomorrowKey}`,
|
|
||||||
{ spaceName: brain.space.name },
|
|
||||||
);
|
|
||||||
expect(topicFacts).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("SM8: sendMessage no longer calls indexFactEmbeddings on every turn (uses per-fact init)", async () => {
|
test("SM8: sendMessage does not call brain.add (no documents added during chat)", async () => {
|
||||||
const brain = await makeBrain(NOOP_EMBEDDING_PROVIDER);
|
const brain = await makeBrain();
|
||||||
let embedManyCalls = 0;
|
const db = brain.db as unknown as MockSupermemory;
|
||||||
const trackingProvider = {
|
const before = db.documentsAddCalls;
|
||||||
model: "track-embed",
|
|
||||||
dimensions: 4,
|
|
||||||
async embed(_input: string): Promise<number[]> {
|
|
||||||
return [0, 0, 0, 0];
|
|
||||||
},
|
|
||||||
async embedMany(inputs: string[]): Promise<number[][]> {
|
|
||||||
embedManyCalls += 1;
|
|
||||||
return inputs.map(() => [0, 0, 0, 0]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Object.defineProperty(brain, "embeddingProvider", {
|
|
||||||
value: trackingProvider,
|
|
||||||
configurable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
chatResponses = [
|
chatResponses = [
|
||||||
{
|
{
|
||||||
@@ -796,40 +905,19 @@ describe("Brain.sendMessage — tool-calling flow", () => {
|
|||||||
[{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "hi" }],
|
[{ sender: "user", time: new Date(2026, 5, 10, 9, 0, 0), content: "hi" }],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
expect(embedManyCalls).toBe(0);
|
expect(db.documentsAddCalls - before).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("SM9: initializeEmbeddings backfills missing embeddings for facts added out-of-band", async () => {
|
test("SM9: out-of-band add() facts are queryable via brain.search without backfill", async () => {
|
||||||
const brain = await makeBrain(SCORING_EMBEDDING_PROVIDER);
|
const brain = await makeBrain();
|
||||||
await brain.db.addFact({
|
await brain.add({
|
||||||
spaceName: brain.space.name,
|
customId: "fact-pizza",
|
||||||
statement: "사용자는 피자를 좋아한다",
|
content: "사용자는 피자를 좋아한다",
|
||||||
summary: "user loves pizza",
|
metadata: { kind: "fact", source: "test" },
|
||||||
source: "test",
|
|
||||||
confidence: 1.0,
|
|
||||||
topics: [
|
|
||||||
{ name: "사용자", category: "entity", granularity: "concrete" },
|
|
||||||
{ name: "피자", category: "concept", granularity: "abstract" },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let preInitHits = await brain.db.searchFacts({
|
const hits = await brain.search("피자", 5);
|
||||||
spaceName: brain.space.name,
|
expect(hits.length).toBeGreaterThan(0);
|
||||||
query: "피자",
|
expect(hits[0]!.content).toContain("피자");
|
||||||
provider: SCORING_EMBEDDING_PROVIDER as never,
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
expect(preInitHits).toHaveLength(0);
|
|
||||||
|
|
||||||
await brain.initializeEmbeddings();
|
|
||||||
|
|
||||||
const postInitHits = await brain.db.searchFacts({
|
|
||||||
spaceName: brain.space.name,
|
|
||||||
query: "피자",
|
|
||||||
provider: SCORING_EMBEDDING_PROVIDER as never,
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
expect(postInitHits.length).toBeGreaterThan(0);
|
|
||||||
expect(postInitHits[0]!.statement).toContain("피자");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import Supermemory from "supermemory";
|
||||||
import { config } from "@/config";
|
import { config } from "@/config";
|
||||||
import {
|
|
||||||
IdentityDB,
|
|
||||||
type EmbeddingProvider,
|
|
||||||
type ExtractedFact,
|
|
||||||
type Space,
|
|
||||||
} from "identitydb";
|
|
||||||
import { llm } from "@/openrouter";
|
import { llm } from "@/openrouter";
|
||||||
import { OpenRouterEmbeddingProvider } from "@/openrouter/embedding";
|
|
||||||
import { loadPrompt } from "@/openrouter/promptLoader";
|
import { loadPrompt } from "@/openrouter/promptLoader";
|
||||||
import {
|
import {
|
||||||
availabilitySchema,
|
availabilitySchema,
|
||||||
@@ -15,11 +9,19 @@ import {
|
|||||||
monthlyScheduleSchema,
|
monthlyScheduleSchema,
|
||||||
type AvailabilityWindows,
|
type AvailabilityWindows,
|
||||||
type DailySchedule,
|
type DailySchedule,
|
||||||
|
type DailySlot,
|
||||||
type MonthlySchedule,
|
type MonthlySchedule,
|
||||||
} from "@/openrouter/schema";
|
} from "@/openrouter/schema";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { factExtractor } from "./factExtractor";
|
import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
|
||||||
|
import type {
|
||||||
|
ChatAssistantMessage,
|
||||||
|
ChatChoice,
|
||||||
|
ChatFunctionTool,
|
||||||
|
ChatMessages,
|
||||||
|
} from "@openrouter/sdk/models";
|
||||||
import { BrainDBManager, brainManager, type BrainItem } from "./manager";
|
import { BrainDBManager, brainManager, type BrainItem } from "./manager";
|
||||||
|
import { MemoryStub } from "./stub";
|
||||||
import {
|
import {
|
||||||
translateMessageHistory,
|
translateMessageHistory,
|
||||||
type MessageHistoryEntry,
|
type MessageHistoryEntry,
|
||||||
@@ -31,13 +33,7 @@ import {
|
|||||||
nextMonth,
|
nextMonth,
|
||||||
pad2,
|
pad2,
|
||||||
} from "./schedule";
|
} from "./schedule";
|
||||||
import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
|
import type { FactInput, FactMetadata, SearchHit, Space } from "./types";
|
||||||
import type {
|
|
||||||
ChatAssistantMessage,
|
|
||||||
ChatChoice,
|
|
||||||
ChatFunctionTool,
|
|
||||||
ChatMessages,
|
|
||||||
} from "@openrouter/sdk/models";
|
|
||||||
|
|
||||||
export interface DebugOptions {
|
export interface DebugOptions {
|
||||||
personality: string;
|
personality: string;
|
||||||
@@ -47,169 +43,96 @@ export interface BrainCreateResult {
|
|||||||
brain: Brain;
|
brain: Brain;
|
||||||
description: string;
|
description: string;
|
||||||
baseSystemPrompt: string;
|
baseSystemPrompt: string;
|
||||||
/**
|
|
||||||
* Raw facts as returned by `factExtractor.extract(description)`. Populated
|
|
||||||
* only when `Brain.create` is called with `debug: true`; in production
|
|
||||||
* (the default), facts are persisted via `db.ingestStatements` which does
|
|
||||||
* not surface the raw extractor output to the caller.
|
|
||||||
*/
|
|
||||||
extractedFacts?: ExtractedFact[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Brain {
|
export class Brain {
|
||||||
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
|
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
|
||||||
private embeddingProvider: EmbeddingProvider;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public db: IdentityDB,
|
public db: Supermemory | MemoryStub,
|
||||||
public space: Space,
|
public space: Space,
|
||||||
public brainbase: BrainItem,
|
public brainbase: BrainItem,
|
||||||
public debug: boolean = false,
|
public debug: boolean = false,
|
||||||
embeddingProvider?: EmbeddingProvider,
|
) {}
|
||||||
) {
|
|
||||||
this.embeddingProvider =
|
// ---------------------------------------------------------------------------
|
||||||
embeddingProvider ?? new OpenRouterEmbeddingProvider();
|
// Memory primitives — thin wrappers over supermemory's `documents` API.
|
||||||
|
//
|
||||||
|
// containerTag = space.name
|
||||||
|
// customId = the stable lookup key (e.g. "daily-schedule:2026-06-10")
|
||||||
|
// content = the fact text or JSON-encoded schedule
|
||||||
|
// metadata = filterable bag: { kind, source, ... }
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async add(input: FactInput): Promise<{ id: string }> {
|
||||||
|
const response = await this.db.documents.add({
|
||||||
|
content: input.content,
|
||||||
|
containerTag: this.space.name,
|
||||||
|
customId: input.customId,
|
||||||
|
metadata: input.metadata,
|
||||||
|
});
|
||||||
|
return { id: response.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get(
|
||||||
|
customId: string,
|
||||||
|
): Promise<{ content: string; metadata: FactMetadata | null } | null> {
|
||||||
|
const listed = await this.db.documents.list({
|
||||||
|
containerTags: [this.space.name],
|
||||||
|
limit: 200,
|
||||||
|
});
|
||||||
|
const match = (listed.memories ?? []).find((m) => m.customId === customId);
|
||||||
|
if (!match) return null;
|
||||||
|
const full = await this.db.documents.get(match.id);
|
||||||
|
return {
|
||||||
|
content: full.content ?? "",
|
||||||
|
metadata: (full.metadata ?? null) as FactMetadata | null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(): Promise<Array<{ customId: string | null; content: string }>> {
|
||||||
|
const listed = await this.db.documents.list({
|
||||||
|
containerTags: [this.space.name],
|
||||||
|
limit: 200,
|
||||||
|
});
|
||||||
|
return (listed.memories ?? []).map((d) => ({
|
||||||
|
customId: d.customId,
|
||||||
|
content: d.content ?? "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string, limit = 5): Promise<SearchHit[]> {
|
||||||
|
const response = await this.db.search.execute({
|
||||||
|
q: query,
|
||||||
|
containerTag: this.space.name,
|
||||||
|
limit,
|
||||||
|
onlyMatchingChunks: true,
|
||||||
|
});
|
||||||
|
return (response.results ?? []).map((r) => {
|
||||||
|
const firstChunk = r.chunks?.[0];
|
||||||
|
return {
|
||||||
|
content: firstChunk?.content ?? "",
|
||||||
|
score: r.score,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Domain methods
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async createDailySchedule(
|
async createDailySchedule(
|
||||||
datetime: Date,
|
datetime: Date,
|
||||||
message: string,
|
message: string,
|
||||||
): Promise<DailySchedule | null> {
|
): Promise<DailySchedule | null> {
|
||||||
try {
|
return await runCreateDailyScheduleSteps(this, datetime, message, noopRunner);
|
||||||
const target = nextDay(datetime);
|
|
||||||
const dateKey = formatDateKey(target);
|
|
||||||
const topicName = `daily-schedule:${dateKey}`;
|
|
||||||
|
|
||||||
const monthlySummary = await this.getMonthlySummaryForDay(target);
|
|
||||||
const history = await this.getHistoryFacts();
|
|
||||||
|
|
||||||
const instruction = await loadPrompt("DAILY_SCHEDULE");
|
|
||||||
const promptMessage = [
|
|
||||||
`Target date: ${dateKey} (${target.toLocaleDateString("en-US", { weekday: "long" })})`,
|
|
||||||
`Personality: ${this.brainbase.baseSystemPrompt}`,
|
|
||||||
monthlySummary
|
|
||||||
? `Monthly summary for this day: ${monthlySummary}`
|
|
||||||
: "(no monthly summary available for this date)",
|
|
||||||
`Recent history (facts):`,
|
|
||||||
history,
|
|
||||||
`User direction: ${message}`,
|
|
||||||
].join("\n\n");
|
|
||||||
|
|
||||||
const schedule = await llm.call<DailySchedule>(llm.models.identity, {
|
|
||||||
instruction,
|
|
||||||
message: promptMessage,
|
|
||||||
jsonSchemaName: "daily-schedule",
|
|
||||||
jsonSchema: dailyScheduleSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.debug) {
|
|
||||||
const fact = await this.db.addFact({
|
|
||||||
spaceName: this.space.name,
|
|
||||||
statement: JSON.stringify(schedule),
|
|
||||||
summary: `Daily schedule for ${dateKey} (${schedule.items.length} slots)`,
|
|
||||||
source: "createDailySchedule",
|
|
||||||
confidence: 1.0,
|
|
||||||
topics: [
|
|
||||||
{
|
|
||||||
name: topicName,
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
role: "schedule",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "daily-schedule",
|
|
||||||
category: "concept",
|
|
||||||
granularity: "abstract",
|
|
||||||
role: "schedule",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: dateKey,
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
role: "date",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await this.indexFactEmbeddingFor(fact);
|
|
||||||
}
|
|
||||||
|
|
||||||
return schedule;
|
|
||||||
} catch (error) {
|
|
||||||
let reason =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message + `(${error.name})`
|
|
||||||
: String(error);
|
|
||||||
if (error instanceof BadRequestResponseError)
|
|
||||||
reason = reason + `${error.body}`;
|
|
||||||
logger.error(`createDailySchedule failed: ${reason}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMonthlySchedule(
|
async createMonthlySchedule(
|
||||||
datetime: Date,
|
datetime: Date,
|
||||||
message: string,
|
message: string,
|
||||||
): Promise<MonthlySchedule | null> {
|
): Promise<MonthlySchedule | null> {
|
||||||
try {
|
return await runCreateMonthlyScheduleSteps(this, datetime, message, noopRunner);
|
||||||
const next = nextMonth(datetime);
|
|
||||||
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
|
|
||||||
const topicName = `monthly-schedule:${monthKey}`;
|
|
||||||
|
|
||||||
const history = await this.getHistoryFacts();
|
|
||||||
|
|
||||||
const instruction = await loadPrompt("MONTHLY_SCHEDULE");
|
|
||||||
const promptMessage = [
|
|
||||||
`Target month: ${monthKey} (${next.daysInMonth} days)`,
|
|
||||||
`Personality: ${this.brainbase.baseSystemPrompt}`,
|
|
||||||
`Recent history (facts):`,
|
|
||||||
history,
|
|
||||||
`User direction: ${message}`,
|
|
||||||
].join("\n\n");
|
|
||||||
|
|
||||||
const schedule = await llm.call<MonthlySchedule>(llm.models.identity, {
|
|
||||||
instruction,
|
|
||||||
message: promptMessage,
|
|
||||||
jsonSchemaName: "monthly-schedule",
|
|
||||||
jsonSchema: monthlyScheduleSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.debug) {
|
|
||||||
const fact = await this.db.addFact({
|
|
||||||
spaceName: this.space.name,
|
|
||||||
statement: JSON.stringify(schedule),
|
|
||||||
summary: `Monthly schedule for ${monthKey} (${schedule.items.length} days)`,
|
|
||||||
source: "createMonthlySchedule",
|
|
||||||
confidence: 1.0,
|
|
||||||
topics: [
|
|
||||||
{
|
|
||||||
name: topicName,
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
role: "schedule",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "monthly-schedule",
|
|
||||||
category: "concept",
|
|
||||||
granularity: "abstract",
|
|
||||||
role: "schedule",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: monthKey,
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
role: "period",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await this.indexFactEmbeddingFor(fact);
|
|
||||||
}
|
|
||||||
|
|
||||||
return schedule;
|
|
||||||
} catch (error) {
|
|
||||||
const reason = error instanceof Error ? error.message : String(error);
|
|
||||||
logger.error(`createMonthlySchedule failed: ${reason}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTodayScheduledAvailability(
|
async getTodayScheduledAvailability(
|
||||||
@@ -220,20 +143,10 @@ export class Brain {
|
|||||||
const cached = this.availabilityCache.get(dateKey);
|
const cached = this.availabilityCache.get(dateKey);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
if (this.debug) {
|
const stored = await this.get(`daily-schedule:${dateKey}`);
|
||||||
logger.warn(
|
if (!stored) return null;
|
||||||
"getTodayScheduledAvailability requires a persisted daily schedule; debug brains have no DB. Use deriveAvailabilityFromSchedule(schedule) instead.",
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const topicName = `daily-schedule:${dateKey}`;
|
const dailySchedule = JSON.parse(stored.content) as DailySchedule;
|
||||||
const facts = await this.db.getTopicFacts(topicName, {
|
|
||||||
spaceName: this.space.name,
|
|
||||||
});
|
|
||||||
if (facts.length === 0) return null;
|
|
||||||
|
|
||||||
const dailySchedule = JSON.parse(facts[0]!.statement) as DailySchedule;
|
|
||||||
const availability =
|
const availability =
|
||||||
await this.deriveAvailabilityFromSchedule(dailySchedule);
|
await this.deriveAvailabilityFromSchedule(dailySchedule);
|
||||||
|
|
||||||
@@ -246,6 +159,30 @@ export class Brain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getCurrentAndAdjacentSlots(now: Date): Promise<DailySlot[]> {
|
||||||
|
const dateKey = formatDateKey(now);
|
||||||
|
const stored = await this.get(`daily-schedule:${dateKey}`);
|
||||||
|
if (!stored) return [];
|
||||||
|
let schedule: DailySchedule;
|
||||||
|
try {
|
||||||
|
schedule = JSON.parse(stored.content) as DailySchedule;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
||||||
|
const toMinutes = (hhmm: string): number => {
|
||||||
|
const [h = 0, m = 0] = hhmm.split(":").map((x) => parseInt(x, 10));
|
||||||
|
return h * 60 + m;
|
||||||
|
};
|
||||||
|
const index = schedule.items.findIndex(
|
||||||
|
(slot) =>
|
||||||
|
toMinutes(slot.start) <= currentMinutes &&
|
||||||
|
currentMinutes < toMinutes(slot.end),
|
||||||
|
);
|
||||||
|
if (index === -1) return [];
|
||||||
|
return schedule.items.slice(Math.max(0, index - 1), index + 2);
|
||||||
|
}
|
||||||
|
|
||||||
async deriveAvailabilityFromSchedule(
|
async deriveAvailabilityFromSchedule(
|
||||||
schedule: DailySchedule,
|
schedule: DailySchedule,
|
||||||
): Promise<AvailabilityWindows> {
|
): Promise<AvailabilityWindows> {
|
||||||
@@ -273,45 +210,6 @@ export class Brain {
|
|||||||
this.availabilityCache.clear();
|
this.availabilityCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Embeds a single fact in the embedding table. Called automatically by
|
|
||||||
* Brain methods that add facts (createDailySchedule, createMonthlySchedule,
|
|
||||||
* Brain.create). Callers who add facts via `db.addFact` directly should
|
|
||||||
* invoke this so the LLM can recall the fact via `searchIdentityDB`. A
|
|
||||||
* no-op in debug mode (where there is no persisted state).
|
|
||||||
*/
|
|
||||||
async indexFactEmbeddingFor(fact: { id: string }): Promise<void> {
|
|
||||||
if (this.debug) return;
|
|
||||||
try {
|
|
||||||
await this.db.indexFactEmbedding(fact.id, {
|
|
||||||
spaceName: this.space.name,
|
|
||||||
provider: this.embeddingProvider,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const reason = error instanceof Error ? error.message : String(error);
|
|
||||||
logger.warn(`indexFactEmbeddingFor(${fact.id}) failed: ${reason}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backfills embeddings for every fact in this brain's space. Intended
|
|
||||||
* for `Brain.create` and `Brain.load` — runs once at initialization so
|
|
||||||
* facts added by older code paths (or out-of-band) become searchable.
|
|
||||||
* No-op in debug mode and when the space has no facts.
|
|
||||||
*/
|
|
||||||
async initializeEmbeddings(): Promise<void> {
|
|
||||||
if (this.debug) return;
|
|
||||||
try {
|
|
||||||
await this.db.indexFactEmbeddings({
|
|
||||||
spaceName: this.space.name,
|
|
||||||
provider: this.embeddingProvider,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const reason = error instanceof Error ? error.message : String(error);
|
|
||||||
logger.warn(`initializeEmbeddings failed: ${reason}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage(
|
async sendMessage(
|
||||||
history: ReadonlyArray<MessageHistoryEntry>,
|
history: ReadonlyArray<MessageHistoryEntry>,
|
||||||
newMessages: ReadonlyArray<MessageHistoryEntry>,
|
newMessages: ReadonlyArray<MessageHistoryEntry>,
|
||||||
@@ -394,7 +292,7 @@ export class Brain {
|
|||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (call.function.name === "searchIdentityDB") {
|
if (call.function.name === "searchMemory") {
|
||||||
const result = await this.executeSearchTool(call.function.arguments);
|
const result = await this.executeSearchTool(call.function.arguments);
|
||||||
messages.push({
|
messages.push({
|
||||||
role: "tool",
|
role: "tool",
|
||||||
@@ -415,7 +313,7 @@ export class Brain {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!hasContent &&
|
!hasContent &&
|
||||||
toolCalls.every((c) => c.function.name === "searchIdentityDB")
|
toolCalls.every((c) => c.function.name === "searchMemory")
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -433,14 +331,23 @@ export class Brain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async buildScheduleBlock(now: Date): Promise<string> {
|
private async buildScheduleBlock(now: Date): Promise<string> {
|
||||||
|
const dateKey = formatDateKey(now);
|
||||||
|
const currentSlots = await this.getCurrentAndAdjacentSlots(now);
|
||||||
|
const currentBlock = currentSlots.length > 0
|
||||||
|
? `Currently (around ${now.toTimeString().slice(0, 5)}):\n${currentSlots
|
||||||
|
.map(
|
||||||
|
(s) =>
|
||||||
|
` ${s.start}-${s.end} ${s.activity}${s.notes ? ` (${s.notes})` : ""}`,
|
||||||
|
)
|
||||||
|
.join("\n")}`
|
||||||
|
: `Currently (${dateKey} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
|
||||||
|
|
||||||
const days: { label: string; date: Date }[] = [
|
const days: { label: string; date: Date }[] = [
|
||||||
{
|
{
|
||||||
label: "Yesterday",
|
label: "Yesterday",
|
||||||
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
|
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
|
||||||
},
|
},
|
||||||
{ label: "Today", date: now },
|
{ label: "Tomorrow",
|
||||||
{
|
|
||||||
label: "Tomorrow",
|
|
||||||
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
|
date: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -452,19 +359,16 @@ export class Brain {
|
|||||||
`${label} (${key}): ${summary ?? "(no daily schedule on file)"}`,
|
`${label} (${key}): ${summary ?? "(no daily schedule on file)"}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return `Schedule context:\n${blocks.join("\n")}`;
|
return `Schedule context:\n${currentBlock}\n\n${blocks.join("\n")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDailyScheduleSummary(
|
private async getDailyScheduleSummary(
|
||||||
dateKey: string,
|
dateKey: string,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (this.debug) return null;
|
|
||||||
try {
|
try {
|
||||||
const facts = await this.db.getTopicFacts(`daily-schedule:${dateKey}`, {
|
const stored = await this.get(`daily-schedule:${dateKey}`);
|
||||||
spaceName: this.space.name,
|
if (!stored) return null;
|
||||||
});
|
const schedule = JSON.parse(stored.content) as DailySchedule;
|
||||||
if (facts.length === 0) return null;
|
|
||||||
const schedule = JSON.parse(facts[0]!.statement) as DailySchedule;
|
|
||||||
const first = schedule.items[0];
|
const first = schedule.items[0];
|
||||||
const last = schedule.items[schedule.items.length - 1];
|
const last = schedule.items[schedule.items.length - 1];
|
||||||
if (!first || !last) return null;
|
if (!first || !last) return null;
|
||||||
@@ -481,15 +385,9 @@ export class Brain {
|
|||||||
return JSON.stringify({ ok: false, error: "missing query" });
|
return JSON.stringify({ ok: false, error: "missing query" });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const hits = await this.db.searchFacts({
|
const hits = await this.search(query, 5);
|
||||||
spaceName: this.space.name,
|
|
||||||
query,
|
|
||||||
provider: this.embeddingProvider,
|
|
||||||
limit: 5,
|
|
||||||
});
|
|
||||||
const compact = hits.map((hit) => ({
|
const compact = hits.map((hit) => ({
|
||||||
statement: hit.statement,
|
content: hit.content,
|
||||||
summary: hit.summary,
|
|
||||||
score: hit.score,
|
score: hit.score,
|
||||||
}));
|
}));
|
||||||
return JSON.stringify({ ok: true, hits: compact });
|
return JSON.stringify({ ok: true, hits: compact });
|
||||||
@@ -499,17 +397,13 @@ export class Brain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMonthlySummaryForDay(target: Date): Promise<string | null> {
|
async getMonthlySummaryForDay(target: Date): Promise<string | null> {
|
||||||
if (this.debug) return null;
|
|
||||||
try {
|
try {
|
||||||
const monthKey = formatMonthKey(target);
|
const monthKey = formatMonthKey(target);
|
||||||
const topicName = `monthly-schedule:${monthKey}`;
|
const stored = await this.get(`monthly-schedule:${monthKey}`);
|
||||||
const facts = await this.db.getTopicFacts(topicName, {
|
if (!stored) return null;
|
||||||
spaceName: this.space.name,
|
|
||||||
});
|
|
||||||
if (facts.length === 0) return null;
|
|
||||||
|
|
||||||
const monthly = JSON.parse(facts[0]!.statement) as MonthlySchedule;
|
const monthly = JSON.parse(stored.content) as MonthlySchedule;
|
||||||
const day = target.getDate();
|
const day = target.getDate();
|
||||||
const entry = monthly.items.find((d) => d.day === day);
|
const entry = monthly.items.find((d) => d.day === day);
|
||||||
return entry?.summary ?? null;
|
return entry?.summary ?? null;
|
||||||
@@ -518,21 +412,13 @@ export class Brain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getHistoryFacts(): Promise<string> {
|
async getHistoryFacts(): Promise<string> {
|
||||||
if (this.debug) return "";
|
|
||||||
try {
|
try {
|
||||||
const topics = await this.db.listTopics({
|
const docs = await this.list();
|
||||||
spaceName: this.space.name,
|
return docs
|
||||||
includeFacts: true,
|
.map((d) => d.content)
|
||||||
});
|
.slice(-30)
|
||||||
const statements: string[] = [];
|
.join("\n");
|
||||||
for (const topic of topics) {
|
|
||||||
const t = topic as { facts?: Array<{ statement: string }> };
|
|
||||||
if (t.facts) {
|
|
||||||
for (const f of t.facts) statements.push(f.statement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return statements.slice(-30).join("\n");
|
|
||||||
} catch {
|
} catch {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -541,26 +427,237 @@ export class Brain {
|
|||||||
static async create(
|
static async create(
|
||||||
displayName: string,
|
displayName: string,
|
||||||
seed: string,
|
seed: string,
|
||||||
options: {
|
options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {},
|
||||||
dbPath?: string;
|
): Promise<BrainCreateResult | null> {
|
||||||
braindbPath?: string;
|
return await runCreateSteps(displayName, seed, options, noopRunner);
|
||||||
debug?: boolean;
|
}
|
||||||
embeddingProvider?: EmbeddingProvider;
|
|
||||||
} = {},
|
static async load(brainId: string): Promise<Brain | null> {
|
||||||
|
const brainbase = await brainManager.loadBrain(brainId);
|
||||||
|
if (!brainbase) return null;
|
||||||
|
|
||||||
|
const db = new Supermemory({ apiKey: config.supermemoryApiKey });
|
||||||
|
const space: Space = { name: brainbase.spaceName };
|
||||||
|
return new Brain(db, space, brainbase);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createDebug(
|
||||||
|
options: DebugOptions,
|
||||||
|
db?: Supermemory | MemoryStub,
|
||||||
|
): Promise<Brain> {
|
||||||
|
const client = db ?? new Supermemory({ apiKey: config.supermemoryApiKey });
|
||||||
|
const space: Space = { name: "brain:debug", description: "Debug Brain" };
|
||||||
|
const brainbase: BrainItem = {
|
||||||
|
brainId: "debug",
|
||||||
|
spaceName: space.name,
|
||||||
|
displayName: "Debug Brain",
|
||||||
|
baseSystemPrompt: options.personality,
|
||||||
|
};
|
||||||
|
return new Brain(client, space, brainbase, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScheduleStep =
|
||||||
|
| { kind: "gather-context" }
|
||||||
|
| {
|
||||||
|
kind: "generate-schedule";
|
||||||
|
jsonSchemaName: string;
|
||||||
|
schedule: DailySchedule | MonthlySchedule;
|
||||||
|
}
|
||||||
|
| { kind: "persist-schedule"; customId: string; contentLength: number }
|
||||||
|
| { kind: "derive-availability"; availability: AvailabilityWindows };
|
||||||
|
|
||||||
|
export type ScheduleProgress = (step: ScheduleStep) => void;
|
||||||
|
|
||||||
|
const noScheduleProgress: ScheduleProgress = () => {};
|
||||||
|
|
||||||
|
export interface StepRunner {
|
||||||
|
start(label: string): void;
|
||||||
|
done(summary: string): void;
|
||||||
|
fail(reason: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noopRunner: StepRunner = {
|
||||||
|
start: () => {},
|
||||||
|
done: () => {},
|
||||||
|
fail: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function runCreateDailyScheduleSteps(
|
||||||
|
brain: Brain,
|
||||||
|
datetime: Date,
|
||||||
|
message: string,
|
||||||
|
runner: StepRunner = noopRunner,
|
||||||
|
): Promise<DailySchedule | null> {
|
||||||
|
try {
|
||||||
|
runner.start("gathering context");
|
||||||
|
const target = nextDay(datetime);
|
||||||
|
const dateKey = formatDateKey(target);
|
||||||
|
const twoDaysAgo = new Date(target);
|
||||||
|
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
|
||||||
|
const twoDaysAgoKey = formatDateKey(twoDaysAgo);
|
||||||
|
const [monthlySummary, history, twoDaysAgoStored] = await Promise.all([
|
||||||
|
brain.getMonthlySummaryForDay(target),
|
||||||
|
brain.getHistoryFacts(),
|
||||||
|
brain.get(`daily-schedule:${twoDaysAgoKey}`),
|
||||||
|
]);
|
||||||
|
let twoDaysAgoSchedule: DailySchedule | null = null;
|
||||||
|
if (twoDaysAgoStored) {
|
||||||
|
try {
|
||||||
|
twoDaysAgoSchedule = JSON.parse(twoDaysAgoStored.content) as DailySchedule;
|
||||||
|
} catch {
|
||||||
|
twoDaysAgoSchedule = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runner.done("");
|
||||||
|
|
||||||
|
runner.start("generating schedule (daily-schedule)");
|
||||||
|
const instruction = await loadPrompt("DAILY_SCHEDULE");
|
||||||
|
const promptMessage = [
|
||||||
|
`Target date: ${dateKey} (${target.toLocaleDateString("en-US", { weekday: "long" })})`,
|
||||||
|
`Personality: ${brain.brainbase.baseSystemPrompt}`,
|
||||||
|
monthlySummary
|
||||||
|
? `Monthly summary for this day: ${monthlySummary}`
|
||||||
|
: "(no monthly summary available for this date)",
|
||||||
|
`Recent schedule (${twoDaysAgoKey}, 2 days ago): ${
|
||||||
|
twoDaysAgoSchedule
|
||||||
|
? twoDaysAgoSchedule.items
|
||||||
|
.map((s) => `${s.start} ${s.activity}`)
|
||||||
|
.join(", ")
|
||||||
|
: "(no schedule on file for 2 days ago)"
|
||||||
|
}`,
|
||||||
|
`Recent history (facts):`,
|
||||||
|
history,
|
||||||
|
`User direction: ${message}`,
|
||||||
|
].join("\n\n");
|
||||||
|
|
||||||
|
const schedule = await llm.call<DailySchedule>(llm.models.identity, {
|
||||||
|
instruction,
|
||||||
|
message: promptMessage,
|
||||||
|
jsonSchemaName: "daily-schedule",
|
||||||
|
jsonSchema: dailyScheduleSchema,
|
||||||
|
});
|
||||||
|
runner.done(`${schedule.items.length} items`);
|
||||||
|
|
||||||
|
runner.start("persisting schedule");
|
||||||
|
await brain.add({
|
||||||
|
customId: `daily-schedule:${dateKey}`,
|
||||||
|
content: JSON.stringify(schedule),
|
||||||
|
metadata: {
|
||||||
|
kind: "schedule",
|
||||||
|
source: "createDailySchedule",
|
||||||
|
date: dateKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
runner.done(`customId=daily-schedule:${dateKey}`);
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
} catch (error) {
|
||||||
|
let reason =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message + `(${error.name})`
|
||||||
|
: String(error);
|
||||||
|
if (error instanceof BadRequestResponseError)
|
||||||
|
reason = reason + `${error.body}`;
|
||||||
|
logger.error(`createDailySchedule failed: ${reason}`);
|
||||||
|
runner.fail(reason);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCreateMonthlyScheduleSteps(
|
||||||
|
brain: Brain,
|
||||||
|
datetime: Date,
|
||||||
|
message: string,
|
||||||
|
runner: StepRunner = noopRunner,
|
||||||
|
): Promise<MonthlySchedule | null> {
|
||||||
|
try {
|
||||||
|
runner.start("gathering context");
|
||||||
|
const next = nextMonth(datetime);
|
||||||
|
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
|
||||||
|
const twoMonthsAgo = new Date(next.year, next.month - 2, 1);
|
||||||
|
const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad2(twoMonthsAgo.getMonth() + 1)}`;
|
||||||
|
const [history, twoMonthsAgoStored] = await Promise.all([
|
||||||
|
brain.getHistoryFacts(),
|
||||||
|
brain.get(`monthly-schedule:${twoMonthsAgoKey}`),
|
||||||
|
]);
|
||||||
|
let twoMonthsAgoSchedule: MonthlySchedule | null = null;
|
||||||
|
if (twoMonthsAgoStored) {
|
||||||
|
try {
|
||||||
|
twoMonthsAgoSchedule = JSON.parse(twoMonthsAgoStored.content) as MonthlySchedule;
|
||||||
|
} catch {
|
||||||
|
twoMonthsAgoSchedule = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runner.done("");
|
||||||
|
|
||||||
|
runner.start("generating schedule (monthly-schedule)");
|
||||||
|
const instruction = await loadPrompt("MONTHLY_SCHEDULE");
|
||||||
|
const promptMessage = [
|
||||||
|
`Target month: ${monthKey} (${next.daysInMonth} days)`,
|
||||||
|
`Personality: ${brain.brainbase.baseSystemPrompt}`,
|
||||||
|
`Recent schedule (${twoMonthsAgoKey}, 2 months ago): ${
|
||||||
|
twoMonthsAgoSchedule
|
||||||
|
? twoMonthsAgoSchedule.items
|
||||||
|
.map((s) => `Day ${s.day}: ${s.summary}`)
|
||||||
|
.join(", ")
|
||||||
|
: "(no schedule on file for 2 months ago)"
|
||||||
|
}`,
|
||||||
|
`Recent history (facts):`,
|
||||||
|
history,
|
||||||
|
`User direction: ${message}`,
|
||||||
|
].join("\n\n");
|
||||||
|
|
||||||
|
const schedule = await llm.call<MonthlySchedule>(llm.models.identity, {
|
||||||
|
instruction,
|
||||||
|
message: promptMessage,
|
||||||
|
jsonSchemaName: "monthly-schedule",
|
||||||
|
jsonSchema: monthlyScheduleSchema,
|
||||||
|
});
|
||||||
|
runner.done(`${schedule.items.length} items`);
|
||||||
|
|
||||||
|
runner.start("persisting schedule");
|
||||||
|
await brain.add({
|
||||||
|
customId: `monthly-schedule:${monthKey}`,
|
||||||
|
content: JSON.stringify(schedule),
|
||||||
|
metadata: {
|
||||||
|
kind: "schedule",
|
||||||
|
source: "createMonthlySchedule",
|
||||||
|
month: monthKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
runner.done(`customId=monthly-schedule:${monthKey}`);
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
} catch (error) {
|
||||||
|
const reason = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error(`createMonthlySchedule failed: ${reason}`);
|
||||||
|
runner.fail(reason);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCreateSteps(
|
||||||
|
displayName: string,
|
||||||
|
seed: string,
|
||||||
|
options: { braindbPath?: string; db?: Supermemory | MemoryStub } = {},
|
||||||
|
runner: StepRunner = noopRunner,
|
||||||
): Promise<BrainCreateResult | null> {
|
): Promise<BrainCreateResult | null> {
|
||||||
const dbPath = options.dbPath ?? config.dbPath;
|
|
||||||
const manager = options.braindbPath
|
const manager = options.braindbPath
|
||||||
? new BrainDBManager(options.braindbPath)
|
? new BrainDBManager(options.braindbPath)
|
||||||
: brainManager;
|
: brainManager;
|
||||||
const embeddingProvider =
|
|
||||||
options.embeddingProvider ?? new OpenRouterEmbeddingProvider();
|
|
||||||
try {
|
try {
|
||||||
|
runner.start("generating persona description (PERSONA_INIT)");
|
||||||
const personaInitInstruction = await loadPrompt("PERSONA_INIT");
|
const personaInitInstruction = await loadPrompt("PERSONA_INIT");
|
||||||
const description = await llm.call<string>(llm.models.identity, {
|
const description = await llm.call<string>(llm.models.identity, {
|
||||||
instruction: personaInitInstruction,
|
instruction: personaInitInstruction,
|
||||||
message: seed,
|
message: seed,
|
||||||
});
|
});
|
||||||
|
runner.done(snippet80(description));
|
||||||
|
|
||||||
|
runner.start(
|
||||||
|
"generating base system prompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)",
|
||||||
|
);
|
||||||
const personaSystemInstruction = await loadPrompt(
|
const personaSystemInstruction = await loadPrompt(
|
||||||
"PERSONA_BASE_SYSTEM_PROMPT",
|
"PERSONA_BASE_SYSTEM_PROMPT",
|
||||||
);
|
);
|
||||||
@@ -576,99 +673,53 @@ export class Brain {
|
|||||||
"PERSONA_BASE_SYSTEM_PROMPT_FIXED",
|
"PERSONA_BASE_SYSTEM_PROMPT_FIXED",
|
||||||
);
|
);
|
||||||
const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`;
|
const baseSystemPrompt = `${generatedBaseSystemPrompt}\n\n${personaSystemFixed}`;
|
||||||
|
runner.done(snippet80(baseSystemPrompt));
|
||||||
|
|
||||||
const db = await IdentityDB.connect({
|
const db =
|
||||||
client: "sqlite",
|
options.db ?? new Supermemory({ apiKey: config.supermemoryApiKey });
|
||||||
filename: dbPath,
|
|
||||||
});
|
|
||||||
await db.initialize();
|
|
||||||
const brainId = randomUUID();
|
const brainId = randomUUID();
|
||||||
const spaceName = `brain:${brainId}`;
|
const space: Space = {
|
||||||
const space = await db.upsertSpace({
|
name: `brain:${brainId}`,
|
||||||
name: spaceName,
|
|
||||||
description: displayName,
|
description: displayName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const brain = new Brain(db, space, {
|
||||||
|
brainId,
|
||||||
|
spaceName: space.name,
|
||||||
|
displayName,
|
||||||
|
baseSystemPrompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
let extractedFacts: ExtractedFact[] | undefined;
|
runner.start("persisting persona document");
|
||||||
if (options.debug) {
|
await brain.add({
|
||||||
extractedFacts = await factExtractor.extract(description);
|
customId: "persona",
|
||||||
for (const fact of extractedFacts) {
|
content: description,
|
||||||
const created = await db.addFact({
|
metadata: { kind: "persona", source: "persona-init" },
|
||||||
spaceName,
|
|
||||||
statement: fact.statement ?? description,
|
|
||||||
summary: fact.summary,
|
|
||||||
source: fact.source,
|
|
||||||
confidence: fact.confidence,
|
|
||||||
topics: fact.topics,
|
|
||||||
metadata: fact.metadata,
|
|
||||||
});
|
});
|
||||||
await db.indexFactEmbedding(created.id, {
|
runner.done(`customId=persona, contentLength=${description.length}`);
|
||||||
spaceName,
|
|
||||||
provider: embeddingProvider,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await db.ingestStatements(description, {
|
|
||||||
extractor: factExtractor,
|
|
||||||
embeddingProvider,
|
|
||||||
spaceName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
runner.start("saving braindb index");
|
||||||
const brainbase: BrainItem = {
|
const brainbase: BrainItem = {
|
||||||
brainId,
|
brainId,
|
||||||
spaceName,
|
spaceName: space.name,
|
||||||
displayName,
|
displayName,
|
||||||
baseSystemPrompt,
|
baseSystemPrompt,
|
||||||
};
|
};
|
||||||
await manager.saveBrain(brainId, brainbase);
|
await manager.saveBrain(brainId, brainbase);
|
||||||
|
runner.done(`brainId=${brainId}`);
|
||||||
|
|
||||||
const brain = new Brain(db, space, brainbase, false, embeddingProvider);
|
return { brain, description, baseSystemPrompt };
|
||||||
return { brain, description, baseSystemPrompt, extractedFacts };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = error instanceof Error ? error.message : String(error);
|
const reason = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(`Failed to create brain "${displayName}": ${reason}`);
|
logger.error(`Failed to create brain "${displayName}": ${reason}`);
|
||||||
|
runner.fail(reason);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async load(brainId: string): Promise<Brain | null> {
|
function snippet80(text: string): string {
|
||||||
const brain = await brainManager.loadBrain(brainId);
|
const flat = text.replace(/\s+/g, " ").trim();
|
||||||
if (!brain) return null;
|
return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
|
||||||
|
|
||||||
const db = await IdentityDB.connect({
|
|
||||||
client: "sqlite",
|
|
||||||
filename: config.dbPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const space = await db.getSpaceByName(brain.spaceName);
|
|
||||||
if (!space) return null;
|
|
||||||
|
|
||||||
const brainInstance = new Brain(db, space, brain);
|
|
||||||
await brainInstance.initializeEmbeddings();
|
|
||||||
return brainInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async createDebug(options: DebugOptions): Promise<Brain> {
|
|
||||||
const db = await IdentityDB.connect({
|
|
||||||
client: "sqlite",
|
|
||||||
filename: ":memory:",
|
|
||||||
});
|
|
||||||
await db.initialize();
|
|
||||||
const space = await db.upsertSpace({
|
|
||||||
name: "debug",
|
|
||||||
description: "Debug Brain",
|
|
||||||
});
|
|
||||||
|
|
||||||
const brainbase: BrainItem = {
|
|
||||||
brainId: "debug",
|
|
||||||
spaceName: "debug",
|
|
||||||
displayName: "Debug Brain",
|
|
||||||
baseSystemPrompt: options.personality,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Brain(db, space, brainbase, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDatetime(now: Date): string {
|
function formatDatetime(now: Date): string {
|
||||||
@@ -699,9 +750,9 @@ function buildSendMessageTools(): ChatFunctionTool[] {
|
|||||||
{
|
{
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
name: "searchIdentityDB",
|
name: "searchMemory",
|
||||||
description:
|
description:
|
||||||
"Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored statements for a natural-language query.",
|
"Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored content for a natural-language query.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
|
|||||||
148
src/brain/stub.ts
Normal file
148
src/brain/stub.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/**
|
||||||
|
* In-memory implementation of the supermemory SDK surface that the
|
||||||
|
* `Brain` class uses. Lets debug commands exercise the full Brain
|
||||||
|
* flow (including `Brain.add`, `Brain.get`, `Brain.search`, etc.)
|
||||||
|
* without any network calls or API key.
|
||||||
|
*
|
||||||
|
* Storage shape: a `Map<id, StoredDoc>` keyed by an internal id; lookups
|
||||||
|
* by `customId` are done by linear scan. `containerTag` is recorded at
|
||||||
|
* `add()` time and filtered on `list()`. `search.execute` does a
|
||||||
|
* case-insensitive substring match against `content`.
|
||||||
|
*
|
||||||
|
* This is NOT a complete supermemory clone — it only implements the
|
||||||
|
* methods Brain calls. Adding a new Brain method that needs a
|
||||||
|
* different SDK method will require extending this stub.
|
||||||
|
*/
|
||||||
|
interface StoredDoc {
|
||||||
|
id: string;
|
||||||
|
customId: string | null;
|
||||||
|
containerTag: string;
|
||||||
|
content: string;
|
||||||
|
summary: string | null;
|
||||||
|
metadata: Record<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MemoryStub {
|
||||||
|
readonly docs = new Map<string, StoredDoc>();
|
||||||
|
private nextId = 0;
|
||||||
|
|
||||||
|
documents = {
|
||||||
|
add: async (params: {
|
||||||
|
content: string;
|
||||||
|
containerTag: string;
|
||||||
|
customId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}) => {
|
||||||
|
const id = `stub-${++this.nextId}`;
|
||||||
|
this.docs.set(id, {
|
||||||
|
id,
|
||||||
|
customId: params.customId ?? null,
|
||||||
|
containerTag: params.containerTag,
|
||||||
|
content: params.content,
|
||||||
|
summary: null,
|
||||||
|
metadata: params.metadata ?? null,
|
||||||
|
});
|
||||||
|
return { id, status: "done" };
|
||||||
|
},
|
||||||
|
list: async (params: { containerTags?: Array<string>; limit?: number }) => {
|
||||||
|
const tags = params.containerTags ?? [];
|
||||||
|
const limit = params.limit ?? 200;
|
||||||
|
const all = Array.from(this.docs.values()).filter((d) =>
|
||||||
|
tags.length === 0 ? true : tags.includes(d.containerTag),
|
||||||
|
);
|
||||||
|
const memories = all.slice(0, limit).map((d) => ({
|
||||||
|
id: d.id,
|
||||||
|
customId: d.customId,
|
||||||
|
containerTag: d.containerTag,
|
||||||
|
content: d.content,
|
||||||
|
summary: d.summary,
|
||||||
|
metadata: d.metadata,
|
||||||
|
createdAt: "2026-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
status: "done" as const,
|
||||||
|
type: "text" as const,
|
||||||
|
connectionId: null,
|
||||||
|
filepath: null,
|
||||||
|
title: null,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
memories,
|
||||||
|
pagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
totalItems: memories.length,
|
||||||
|
totalPages: 1,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
get: async (id: string) => {
|
||||||
|
const d = this.docs.get(id);
|
||||||
|
if (!d) {
|
||||||
|
throw new Error(`MemoryStub.documents.get: no such id ${id}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: d.id,
|
||||||
|
customId: d.customId,
|
||||||
|
containerTag: d.containerTag,
|
||||||
|
content: d.content,
|
||||||
|
summary: d.summary,
|
||||||
|
metadata: d.metadata,
|
||||||
|
createdAt: "2026-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
status: "done" as const,
|
||||||
|
type: "text" as const,
|
||||||
|
connectionId: null,
|
||||||
|
filepath: null,
|
||||||
|
title: null,
|
||||||
|
source: null,
|
||||||
|
ogImage: null,
|
||||||
|
raw: null,
|
||||||
|
spatialPoint: null,
|
||||||
|
taskType: "memory" as const,
|
||||||
|
url: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
search = {
|
||||||
|
execute: async (params: {
|
||||||
|
q: string;
|
||||||
|
containerTag?: string;
|
||||||
|
limit?: number;
|
||||||
|
onlyMatchingChunks?: boolean;
|
||||||
|
}) => {
|
||||||
|
const q = params.q.toLowerCase();
|
||||||
|
const limit = params.limit ?? 5;
|
||||||
|
const hits = Array.from(this.docs.values())
|
||||||
|
.filter(
|
||||||
|
(d) =>
|
||||||
|
(params.containerTag
|
||||||
|
? d.containerTag === params.containerTag
|
||||||
|
: true) && d.content.toLowerCase().includes(q),
|
||||||
|
)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map((d, i) => ({
|
||||||
|
chunks: [
|
||||||
|
{
|
||||||
|
content: d.content,
|
||||||
|
isRelevant: true,
|
||||||
|
score: 1 - i * 0.1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
summary: d.summary,
|
||||||
|
score: 1 - i * 0.1,
|
||||||
|
documentId: d.id,
|
||||||
|
metadata: d.metadata as Record<string, unknown> | null,
|
||||||
|
createdAt: "2026-01-01T00:00:00Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00Z",
|
||||||
|
title: d.customId,
|
||||||
|
type: "text" as const,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
results: hits,
|
||||||
|
total: hits.length,
|
||||||
|
timing: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
26
src/brain/types.ts
Normal file
26
src/brain/types.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/** A brain's logical namespace. Maps to supermemory's `containerTag`. */
|
||||||
|
export interface Space {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Metadata bag stored alongside a document in supermemory. */
|
||||||
|
export type FactMetadata = Record<string, string | number | boolean | string[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fact/document to store in the brain's memory. Mirrors supermemory's
|
||||||
|
* `documents.add` parameter shape: `content` is the canonical text (or
|
||||||
|
* JSON-encoded schedule), `customId` is the stable lookup key, and
|
||||||
|
* `metadata` is the filterable bag.
|
||||||
|
*/
|
||||||
|
export interface FactInput {
|
||||||
|
customId: string;
|
||||||
|
content: string;
|
||||||
|
metadata?: FactMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A single semantic-search hit. */
|
||||||
|
export interface SearchHit {
|
||||||
|
content: string;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { existsSync, readdirSync } from "fs";
|
import { existsSync, readdirSync } from "fs";
|
||||||
import { unlink, writeFile } from "fs/promises";
|
|
||||||
import type { ExtractedFact } from "identitydb";
|
|
||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
|
|
||||||
interface RecordedCall {
|
interface RecordedCall {
|
||||||
@@ -19,36 +16,6 @@ const llmCalls: RecordedCall[] = [];
|
|||||||
const PERSONA_DESCRIPTION = "A 34yo night-shift nurse, hides exhaustion behind sarcasm.";
|
const PERSONA_DESCRIPTION = "A 34yo night-shift nurse, hides exhaustion behind sarcasm.";
|
||||||
const GENERATED_BASE_SYSTEM_PROMPT =
|
const GENERATED_BASE_SYSTEM_PROMPT =
|
||||||
"You are Maren. You text in lowercase. You use '...' when tired.";
|
"You are Maren. You text in lowercase. You use '...' when tired.";
|
||||||
const EXTRACTED_FACTS: ExtractedFact[] = [
|
|
||||||
{
|
|
||||||
statement: "Maren is 34 years old.",
|
|
||||||
summary: "Maren is 34 years old.",
|
|
||||||
source: "persona-init",
|
|
||||||
confidence: 1.0,
|
|
||||||
topics: [
|
|
||||||
{
|
|
||||||
name: "maren-age",
|
|
||||||
category: "temporal",
|
|
||||||
granularity: "concrete",
|
|
||||||
role: "attribute",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
statement: "Maren is a night-shift nurse.",
|
|
||||||
summary: "Maren is a night-shift nurse.",
|
|
||||||
source: "persona-init",
|
|
||||||
confidence: 1.0,
|
|
||||||
topics: [
|
|
||||||
{
|
|
||||||
name: "maren-occupation",
|
|
||||||
category: "entity",
|
|
||||||
granularity: "concrete",
|
|
||||||
role: "attribute",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
|
const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
|
||||||
llmCalls.push({ model, options });
|
llmCalls.push({ model, options });
|
||||||
@@ -64,9 +31,6 @@ const mockCall = mock(async <T>(model: unknown, options: any): Promise<T> => {
|
|||||||
) {
|
) {
|
||||||
return GENERATED_BASE_SYSTEM_PROMPT as unknown as T;
|
return GENERATED_BASE_SYSTEM_PROMPT as unknown as T;
|
||||||
}
|
}
|
||||||
if (options.jsonSchemaName === "fact-extractor") {
|
|
||||||
return { items: EXTRACTED_FACTS } as unknown as T;
|
|
||||||
}
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`,
|
`unexpected LLM call: model=${model} instruction=${options.instruction?.slice(0, 80)}`,
|
||||||
);
|
);
|
||||||
@@ -82,26 +46,12 @@ mock.module("@/openrouter", () => ({
|
|||||||
mock.module("@/config", () => ({
|
mock.module("@/config", () => ({
|
||||||
config: {
|
config: {
|
||||||
openrouterApiKey: "test-key",
|
openrouterApiKey: "test-key",
|
||||||
dbPath: ":memory:",
|
supermemoryApiKey: "test-supermemory-key",
|
||||||
braindbPath: "/tmp/brainbox-test-braindb-debug-brain-IGNORED.json",
|
braindbPath: "/tmp/brainbox-test-braindb-debug-brain-IGNORED.json",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
mock.module("@/openrouter/embedding", () => ({
|
|
||||||
OpenRouterEmbeddingProvider: class {
|
|
||||||
model = "test-embed";
|
|
||||||
dimensions = 4;
|
|
||||||
async embed(_input: string): Promise<number[]> {
|
|
||||||
return [0, 0, 0, 0];
|
|
||||||
}
|
|
||||||
async embedMany(inputs: string[]): Promise<number[][]> {
|
|
||||||
return inputs.map(() => [0, 0, 0, 0]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { runDebugBrainInit } = await import("./brain");
|
const { runDebugBrainInit } = await import("./brain");
|
||||||
const { Brain: ProdBrain } = await import("@/brain");
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
llmCalls.length = 0;
|
llmCalls.length = 0;
|
||||||
@@ -109,22 +59,23 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
const { unlink } = await import("fs/promises");
|
||||||
const tmpFiles = readdirSync(tmpdir()).filter((f) =>
|
const tmpFiles = readdirSync(tmpdir()).filter((f) =>
|
||||||
f.startsWith("brainbox-debug-brain-"),
|
f.startsWith("brainbox-debug-brain-"),
|
||||||
);
|
);
|
||||||
for (const f of tmpFiles) {
|
for (const f of tmpFiles) {
|
||||||
try {
|
try {
|
||||||
const { unlink } = await import("fs/promises");
|
|
||||||
await unlink(`${tmpdir()}/${f}`);
|
await unlink(`${tmpdir()}/${f}`);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("runDebugBrainInit", () => {
|
describe("runDebugBrainInit", () => {
|
||||||
test("B1: returns ok result with full description, baseSystemPrompt, extractedFacts, and uses the supplied seed", async () => {
|
test("B1: returns ok result with full description, baseSystemPrompt, storedFacts, and uses the supplied seed", async () => {
|
||||||
const result = await runDebugBrainInit({
|
const result = await runDebugBrainInit({
|
||||||
displayName: "Maren",
|
displayName: "Maren",
|
||||||
seed: "Maren, 34, night-shift nurse, hides exhaustion behind sarcasm",
|
seed: "Maren, 34, night-shift nurse, hides exhaustion behind sarcasm",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
@@ -148,18 +99,22 @@ describe("runDebugBrainInit", () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.extractedFacts).toEqual(EXTRACTED_FACTS);
|
expect(result.storedFacts).toHaveLength(1);
|
||||||
|
expect(result.storedFacts[0]!.customId).toBe("persona");
|
||||||
|
expect(result.storedFacts[0]!.content).toContain(PERSONA_DESCRIPTION);
|
||||||
|
|
||||||
expect(typeof result.elapsedMs).toBe("number");
|
expect(typeof result.elapsedMs).toBe("number");
|
||||||
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
|
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("B2: invokes the LLM exactly 3 times — PERSONA_INIT, PERSONA_BASE_SYSTEM_PROMPT, fact-extractor", async () => {
|
test("B2: invokes the LLM exactly 2 times — PERSONA_INIT and PERSONA_BASE_SYSTEM_PROMPT", async () => {
|
||||||
await runDebugBrainInit({
|
await runDebugBrainInit({
|
||||||
displayName: "Test",
|
displayName: "Test",
|
||||||
seed: "a seed",
|
seed: "a seed",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(llmCalls.length).toBe(3);
|
expect(llmCalls.length).toBe(2);
|
||||||
|
|
||||||
const initCall = llmCalls[0]!;
|
const initCall = llmCalls[0]!;
|
||||||
expect(initCall.options.message).toBe("a seed");
|
expect(initCall.options.message).toBe("a seed");
|
||||||
@@ -168,32 +123,28 @@ describe("runDebugBrainInit", () => {
|
|||||||
const systemCall = llmCalls[1]!;
|
const systemCall = llmCalls[1]!;
|
||||||
expect(systemCall.options.jsonSchemaName).toBeUndefined();
|
expect(systemCall.options.jsonSchemaName).toBeUndefined();
|
||||||
expect(systemCall.options.message).toBe(PERSONA_DESCRIPTION);
|
expect(systemCall.options.message).toBe(PERSONA_DESCRIPTION);
|
||||||
|
|
||||||
const factCall = llmCalls[2]!;
|
|
||||||
expect(factCall.options.jsonSchemaName).toBe("fact-extractor");
|
|
||||||
expect(factCall.options.message).toBe(PERSONA_DESCRIPTION);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("B3: writes no real on-disk state — no brainbox.db, no brainbox.json, no leftover temp braindb in /tmp", async () => {
|
test("B3: writes no real on-disk state — no leftover temp braindb in /tmp, no stray files in cwd", async () => {
|
||||||
const cwd = process.cwd();
|
const cwd = process.cwd();
|
||||||
|
|
||||||
const beforeDb = existsSync(`${cwd}/brainbox.db`);
|
const beforeCwdEntries = readdirSync(cwd);
|
||||||
const beforeJson = existsSync(`${cwd}/brainbox.json`);
|
|
||||||
const beforeTmp = readdirSync(tmpdir()).filter((f) =>
|
const beforeTmp = readdirSync(tmpdir()).filter((f) =>
|
||||||
f.startsWith("brainbox-debug-brain-"),
|
f.startsWith("brainbox-debug-brain-"),
|
||||||
);
|
);
|
||||||
|
|
||||||
await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x" });
|
await runDebugBrainInit({ displayName: "NoDiskCheck", seed: "x", noSupermemory: true });
|
||||||
|
|
||||||
const afterDb = existsSync(`${cwd}/brainbox.db`);
|
const afterCwdEntries = readdirSync(cwd);
|
||||||
const afterJson = existsSync(`${cwd}/brainbox.json`);
|
|
||||||
const afterTmp = readdirSync(tmpdir()).filter((f) =>
|
const afterTmp = readdirSync(tmpdir()).filter((f) =>
|
||||||
f.startsWith("brainbox-debug-brain-"),
|
f.startsWith("brainbox-debug-brain-"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(afterDb).toBe(beforeDb);
|
expect(afterCwdEntries).toEqual(beforeCwdEntries);
|
||||||
expect(afterJson).toBe(beforeJson);
|
|
||||||
expect(afterTmp).toHaveLength(0);
|
expect(afterTmp).toHaveLength(0);
|
||||||
|
|
||||||
|
expect(existsSync(`${cwd}/brainbox.db`)).toBe(false);
|
||||||
|
expect(existsSync(`${cwd}/brainbox.json`)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("B4: when Brain.create returns null (e.g. LLM throws), result is {ok: false, error}", async () => {
|
test("B4: when Brain.create returns null (e.g. LLM throws), result is {ok: false, error}", async () => {
|
||||||
@@ -204,6 +155,7 @@ describe("runDebugBrainInit", () => {
|
|||||||
const result = await runDebugBrainInit({
|
const result = await runDebugBrainInit({
|
||||||
displayName: "Doomed",
|
displayName: "Doomed",
|
||||||
seed: "x",
|
seed: "x",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
@@ -213,10 +165,11 @@ describe("runDebugBrainInit", () => {
|
|||||||
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
|
expect(result.elapsedMs).toBeGreaterThanOrEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("B5: with no DB_PATH / BRAINDB_PATH env, runDebugBrainInit still works (no env dependency)", async () => {
|
test("B5: with no BRAINDB_PATH env, runDebugBrainInit still works (no env dependency)", async () => {
|
||||||
const result = await runDebugBrainInit({
|
const result = await runDebugBrainInit({
|
||||||
displayName: "EnvFree",
|
displayName: "EnvFree",
|
||||||
seed: "no env",
|
seed: "no env",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
if (!result.ok) throw new Error("expected ok");
|
if (!result.ok) throw new Error("expected ok");
|
||||||
@@ -225,48 +178,12 @@ describe("runDebugBrainInit", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Brain.create (production path — debug: false)", () => {
|
// ---------------------------------------------------------------------------
|
||||||
test("B6: with debug omitted (default), uses db.ingestStatements and does NOT return extractedFacts", async () => {
|
// Removed: B6 and B7 (production path with `debug: true|false` option).
|
||||||
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
|
//
|
||||||
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
|
// Reason: `Brain.create` no longer accepts a `debug` option. The production
|
||||||
|
// path is now identical to the debug path — `Brain.create` always persists
|
||||||
const result = await ProdBrain.create("ProdMaren", "a prod seed", {
|
// facts to supermemory and returns `{ brain, description, baseSystemPrompt }`
|
||||||
dbPath: ":memory:",
|
// (no `extractedFacts`). B1 already exercises the post-refactor production
|
||||||
braindbPath,
|
// behavior end-to-end through `runDebugBrainInit`.
|
||||||
});
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
try {
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
if (!result) throw new Error("expected result");
|
|
||||||
expect(result.brain).toBeDefined();
|
|
||||||
expect(result.description).toBe(PERSONA_DESCRIPTION);
|
|
||||||
expect(result.baseSystemPrompt).toContain(GENERATED_BASE_SYSTEM_PROMPT);
|
|
||||||
expect(result.extractedFacts).toBeUndefined();
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await unlink(braindbPath);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("B7: with debug: false (explicit), same as default — uses db.ingestStatements, no extractedFacts", async () => {
|
|
||||||
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
|
|
||||||
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
|
|
||||||
|
|
||||||
const result = await ProdBrain.create("ProdMaren2", "seed", {
|
|
||||||
dbPath: ":memory:",
|
|
||||||
braindbPath,
|
|
||||||
debug: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
if (!result) throw new Error("expected result");
|
|
||||||
expect(result.extractedFacts).toBeUndefined();
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await unlink(braindbPath);
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ import { unlink, writeFile } from "fs/promises";
|
|||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import ora from "ora";
|
import { runCreateSteps } from "@/brain";
|
||||||
import type { ExtractedFact } from "identitydb";
|
import { MemoryStub } from "@/brain/stub";
|
||||||
import { Brain } from "@/brain";
|
|
||||||
import { formatDuration } from "@/utils/duration";
|
import { formatDuration } from "@/utils/duration";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
|
import {
|
||||||
|
StepDriver,
|
||||||
|
printKeyValue,
|
||||||
|
printSection,
|
||||||
|
} from "./output";
|
||||||
|
|
||||||
export interface BrainInitOptions {
|
export interface BrainInitOptions {
|
||||||
displayName: string;
|
displayName: string;
|
||||||
seed: string;
|
seed: string;
|
||||||
|
noSupermemory: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrainInitResult =
|
export type BrainInitResult =
|
||||||
@@ -23,26 +28,12 @@ export type BrainInitResult =
|
|||||||
spaceName: string;
|
spaceName: string;
|
||||||
description: string;
|
description: string;
|
||||||
baseSystemPrompt: string;
|
baseSystemPrompt: string;
|
||||||
extractedFacts: ExtractedFact[];
|
storedFacts: Array<{ customId: string | null; content: string }>;
|
||||||
|
storageMode: "supermemory" | "stub";
|
||||||
elapsedMs: number;
|
elapsedMs: number;
|
||||||
}
|
}
|
||||||
| { ok: false; error: string; elapsedMs: number };
|
| { ok: false; error: string; elapsedMs: number };
|
||||||
|
|
||||||
/**
|
|
||||||
* Exercise the full `Brain.create` flow (PERSONA_INIT → PERSONA_BASE_SYSTEM_PROMPT
|
|
||||||
* LLM calls → SQLite DB upsert → fact extraction via `factExtractor.extract` →
|
|
||||||
* braindb save) without touching real on-disk state.
|
|
||||||
*
|
|
||||||
* - SQLite DB uses `:memory:` (ephemeral, dies with the process).
|
|
||||||
* - The braindb JSON is written to a fresh temp file under `os.tmpdir()`
|
|
||||||
* and unlinked after the run.
|
|
||||||
*
|
|
||||||
* Prints the full text of:
|
|
||||||
* 1. the generated `description` (PERSONA_INIT output)
|
|
||||||
* 2. the concatenated `baseSystemPrompt` (generated + fixed)
|
|
||||||
* 3. the `extractedFacts` (obtained by directly calling
|
|
||||||
* `factExtractor.extract(description)`)
|
|
||||||
*/
|
|
||||||
export async function runDebugBrainInit(
|
export async function runDebugBrainInit(
|
||||||
opts: BrainInitOptions,
|
opts: BrainInitOptions,
|
||||||
): Promise<BrainInitResult> {
|
): Promise<BrainInitResult> {
|
||||||
@@ -52,56 +43,53 @@ export async function runDebugBrainInit(
|
|||||||
`brainbox-debug-brain-${randomUUID()}.json`,
|
`brainbox-debug-brain-${randomUUID()}.json`,
|
||||||
);
|
);
|
||||||
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
|
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
|
||||||
const spinner = ora(
|
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
|
||||||
`Initializing brain "${opts.displayName}" with LLM (debug, no real disk state)...`,
|
const db = opts.noSupermemory ? new MemoryStub() : undefined;
|
||||||
).start();
|
|
||||||
try {
|
try {
|
||||||
const result = await Brain.create(opts.displayName, opts.seed, {
|
const steps = new StepDriver(4);
|
||||||
dbPath: ":memory:",
|
|
||||||
|
const result = await runCreateSteps(opts.displayName, opts.seed, {
|
||||||
braindbPath,
|
braindbPath,
|
||||||
debug: true,
|
db,
|
||||||
});
|
}, steps);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
spinner.fail("Brain initialization failed");
|
|
||||||
const elapsedMs = Date.now() - startTime;
|
const elapsedMs = Date.now() - startTime;
|
||||||
return { ok: false, error: "Brain initialization failed", elapsedMs };
|
return { ok: false, error: "Brain initialization failed", elapsedMs };
|
||||||
}
|
}
|
||||||
const {
|
const { brain, description, baseSystemPrompt } = result;
|
||||||
brain,
|
const storedFacts = await brain.list();
|
||||||
description,
|
|
||||||
baseSystemPrompt,
|
|
||||||
extractedFacts,
|
|
||||||
} = result;
|
|
||||||
const factCount = extractedFacts?.length ?? 0;
|
|
||||||
spinner.succeed(
|
|
||||||
`Brain initialized (id=${brain.brainbase.brainId}, space=${brain.brainbase.spaceName}, ${factCount} fact(s) extracted)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
printSection(`Description (PERSONA_INIT output)`);
|
console.log();
|
||||||
|
printSection(`Brain — ${brain.brainbase.displayName}`);
|
||||||
|
printKeyValue({
|
||||||
|
brainId: brain.brainbase.brainId,
|
||||||
|
spaceName: brain.brainbase.spaceName,
|
||||||
|
storage: storageMode,
|
||||||
|
documents: String(storedFacts.length),
|
||||||
|
});
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
printSection(`Step 1 output — Description (PERSONA_INIT)`);
|
||||||
console.log(description);
|
console.log(description);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
printSection(`baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
|
printSection(`Step 2 output — baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
|
||||||
console.log(baseSystemPrompt);
|
console.log(baseSystemPrompt);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
printSection(
|
printSection(`Step 3 output — Stored documents (brain.list() — ${storedFacts.length})`);
|
||||||
`Extracted facts (factExtractor.extract — ${factCount})`,
|
if (storedFacts.length > 0) {
|
||||||
);
|
storedFacts.forEach((doc, i) => {
|
||||||
if (extractedFacts && extractedFacts.length > 0) {
|
console.log();
|
||||||
extractedFacts.forEach((fact, i) => {
|
console.log(`[${i + 1}/${storedFacts.length}]`);
|
||||||
console.log(`\n[${i + 1}/${extractedFacts.length}]`);
|
printKeyValue({
|
||||||
console.log(` statement: ${fact.statement ?? ""}`);
|
customId: doc.customId ?? "(none)",
|
||||||
console.log(` summary: ${fact.summary ?? ""}`);
|
content: doc.content,
|
||||||
console.log(` source: ${fact.source ?? ""}`);
|
});
|
||||||
console.log(` confidence: ${fact.confidence ?? ""}`);
|
|
||||||
console.log(` topics: ${JSON.stringify(fact.topics)}`);
|
|
||||||
if (fact.metadata) {
|
|
||||||
console.log(` metadata: ${JSON.stringify(fact.metadata)}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(" (no facts extracted)");
|
console.log(" (no documents stored)");
|
||||||
}
|
}
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
@@ -118,14 +106,10 @@ export async function runDebugBrainInit(
|
|||||||
spaceName: brain.brainbase.spaceName,
|
spaceName: brain.brainbase.spaceName,
|
||||||
description,
|
description,
|
||||||
baseSystemPrompt,
|
baseSystemPrompt,
|
||||||
extractedFacts: extractedFacts ?? [],
|
storedFacts,
|
||||||
|
storageMode,
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
const reason = error instanceof Error ? error.message : String(error);
|
|
||||||
spinner.fail("Brain initialization failed");
|
|
||||||
const elapsedMs = Date.now() - startTime;
|
|
||||||
return { ok: false, error: reason, elapsedMs };
|
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
await unlink(braindbPath);
|
await unlink(braindbPath);
|
||||||
@@ -136,37 +120,35 @@ export async function runDebugBrainInit(
|
|||||||
export function addBrainSubcommand(parent: Command): Command {
|
export function addBrainSubcommand(parent: Command): Command {
|
||||||
const cmd = parent
|
const cmd = parent
|
||||||
.command("brain")
|
.command("brain")
|
||||||
.description(
|
.description("Debug tools for brain lifecycle (no real disk writes)");
|
||||||
"Debug tools for brain lifecycle (no real disk writes)",
|
|
||||||
);
|
|
||||||
|
|
||||||
cmd
|
cmd
|
||||||
.command("init")
|
.command("init")
|
||||||
.description(
|
.description(
|
||||||
"Initialize a new brain with LLM (in-memory DB, temp braindb; nothing persisted)",
|
"Initialize a new brain with LLM (temp braindb; nothing persisted to repo)",
|
||||||
)
|
)
|
||||||
.requiredOption("-n, --name <text>", "Display name for the new brain")
|
.requiredOption("-n, --name <text>", "Display name for the new brain")
|
||||||
.requiredOption(
|
.requiredOption(
|
||||||
"-s, --seed <text>",
|
"-s, --seed <text>",
|
||||||
"Seed text used to generate the persona biography",
|
"Seed text used to generate the persona biography",
|
||||||
)
|
)
|
||||||
.action(async (opts: { name: string; seed: string }) => {
|
.option(
|
||||||
|
"--no-supermemory",
|
||||||
|
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
|
||||||
|
)
|
||||||
|
.action(
|
||||||
|
async (opts: { name: string; seed: string; supermemory: boolean }) => {
|
||||||
const result = await runDebugBrainInit({
|
const result = await runDebugBrainInit({
|
||||||
displayName: opts.name,
|
displayName: opts.name,
|
||||||
seed: opts.seed,
|
seed: opts.seed,
|
||||||
|
noSupermemory: opts.supermemory === false,
|
||||||
});
|
});
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
logger.error(result.error);
|
logger.error(result.error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printSection(title: string): void {
|
|
||||||
const line = "─".repeat(Math.max(40, title.length + 4));
|
|
||||||
console.log(`\n┌${line}┐`);
|
|
||||||
console.log(`│ ${title}`);
|
|
||||||
console.log(`└${line}┘`);
|
|
||||||
}
|
|
||||||
|
|||||||
63
src/commands/debug/output.ts
Normal file
63
src/commands/debug/output.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import ora, { type Ora } from "ora";
|
||||||
|
|
||||||
|
export function printSection(title: string): void {
|
||||||
|
const line = "─".repeat(Math.max(40, title.length + 4));
|
||||||
|
console.log(`\n┌${line}┐`);
|
||||||
|
console.log(`│ ${title}`);
|
||||||
|
console.log(`└${line}┘`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printKeyValue(pairs: Record<string, string>): void {
|
||||||
|
const labelWidth = Math.max(...Object.keys(pairs).map((k) => k.length));
|
||||||
|
for (const [key, value] of Object.entries(pairs)) {
|
||||||
|
console.log(` ${key.padEnd(labelWidth)} ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StepDriver {
|
||||||
|
private readonly stepCount: number;
|
||||||
|
private stepIndex = 0;
|
||||||
|
private current: Ora | null = null;
|
||||||
|
private currentLabel = "";
|
||||||
|
|
||||||
|
constructor(stepCount: number) {
|
||||||
|
this.stepCount = stepCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(label: string): void {
|
||||||
|
this.stepIndex += 1;
|
||||||
|
this.resolvePrevious();
|
||||||
|
this.currentLabel = label;
|
||||||
|
const text = `Step ${this.stepIndex}/${this.stepCount}: ${label}`;
|
||||||
|
this.current = ora(text).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
done(summary: string): void {
|
||||||
|
if (!this.current) return;
|
||||||
|
const text = this.current.text;
|
||||||
|
this.current.succeed(`${text} — ${summary}`);
|
||||||
|
this.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fail(reason: string): void {
|
||||||
|
if (!this.current) {
|
||||||
|
console.log(`${chalk.red("✖")} ${this.currentLabel} — ${reason}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.current.fail(`${this.current.text} — ${reason}`);
|
||||||
|
this.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePrevious(): void {
|
||||||
|
if (this.current) {
|
||||||
|
this.current.stop();
|
||||||
|
this.current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snippet(text: string): string {
|
||||||
|
const flat = text.replace(/\s+/g, " ").trim();
|
||||||
|
return flat.length > 80 ? `${flat.slice(0, 77)}...` : flat;
|
||||||
|
}
|
||||||
@@ -101,6 +101,7 @@ describe("runDebugScheduleDaily", () => {
|
|||||||
const result = await runDebugScheduleDaily({
|
const result = await runDebugScheduleDaily({
|
||||||
message: "focus on writing",
|
message: "focus on writing",
|
||||||
personality: "test-personality-XYZ",
|
personality: "test-personality-XYZ",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
@@ -131,6 +132,7 @@ describe("runDebugScheduleDaily", () => {
|
|||||||
const result = await runDebugScheduleDaily({
|
const result = await runDebugScheduleDaily({
|
||||||
message: "",
|
message: "",
|
||||||
personality: "p",
|
personality: "p",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
if (result.ok) throw new Error("expected !ok");
|
if (result.ok) throw new Error("expected !ok");
|
||||||
@@ -145,6 +147,7 @@ describe("runDebugScheduleMonthly", () => {
|
|||||||
const result = await runDebugScheduleMonthly({
|
const result = await runDebugScheduleMonthly({
|
||||||
message: "study for GRE",
|
message: "study for GRE",
|
||||||
personality: "test-personality-ABC",
|
personality: "test-personality-ABC",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.ok).toBe(true);
|
expect(result.ok).toBe(true);
|
||||||
@@ -169,6 +172,7 @@ describe("runDebugScheduleMonthly", () => {
|
|||||||
const result = await runDebugScheduleMonthly({
|
const result = await runDebugScheduleMonthly({
|
||||||
message: "",
|
message: "",
|
||||||
personality: "p",
|
personality: "p",
|
||||||
|
noSupermemory: true,
|
||||||
});
|
});
|
||||||
expect(result.ok).toBe(false);
|
expect(result.ok).toBe(false);
|
||||||
if (result.ok) throw new Error("expected !ok");
|
if (result.ok) throw new Error("expected !ok");
|
||||||
@@ -186,7 +190,7 @@ describe("debug schedule no-disk invariant", () => {
|
|||||||
const beforeDb = existsSync(resolve(process.cwd(), "brainbox.db"));
|
const beforeDb = existsSync(resolve(process.cwd(), "brainbox.db"));
|
||||||
const beforeJson = existsSync(resolve(process.cwd(), "brainbox.json"));
|
const beforeJson = existsSync(resolve(process.cwd(), "brainbox.json"));
|
||||||
|
|
||||||
await runDebugScheduleDaily({ message: "m", personality: "p" });
|
await runDebugScheduleDaily({ message: "m", personality: "p", noSupermemory: true });
|
||||||
|
|
||||||
const afterDb = existsSync(resolve(process.cwd(), "brainbox.db"));
|
const afterDb = existsSync(resolve(process.cwd(), "brainbox.db"));
|
||||||
const afterJson = existsSync(resolve(process.cwd(), "brainbox.json"));
|
const afterJson = existsSync(resolve(process.cwd(), "brainbox.json"));
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import ora from "ora";
|
import {
|
||||||
import { Brain } from "@/brain";
|
Brain,
|
||||||
|
runCreateDailyScheduleSteps,
|
||||||
|
runCreateMonthlyScheduleSteps,
|
||||||
|
} from "@/brain";
|
||||||
|
import { MemoryStub } from "@/brain/stub";
|
||||||
import {
|
import {
|
||||||
type AvailabilityWindows,
|
type AvailabilityWindows,
|
||||||
type DailySchedule,
|
type DailySchedule,
|
||||||
@@ -9,10 +13,16 @@ import {
|
|||||||
import { formatDuration } from "@/utils/duration";
|
import { formatDuration } from "@/utils/duration";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { formatDateKey, nextMonth, pad2 } from "@/brain/schedule";
|
import { formatDateKey, nextMonth, pad2 } from "@/brain/schedule";
|
||||||
|
import {
|
||||||
|
StepDriver,
|
||||||
|
printKeyValue,
|
||||||
|
printSection,
|
||||||
|
} from "./output";
|
||||||
|
|
||||||
export interface ScheduleOptions {
|
export interface ScheduleOptions {
|
||||||
message: string;
|
message: string;
|
||||||
personality: string;
|
personality: string;
|
||||||
|
noSupermemory: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DailyRunResult =
|
export type DailyRunResult =
|
||||||
@@ -23,6 +33,7 @@ export type DailyRunResult =
|
|||||||
tomorrow: Date;
|
tomorrow: Date;
|
||||||
schedule: DailySchedule;
|
schedule: DailySchedule;
|
||||||
availability: AvailabilityWindows;
|
availability: AvailabilityWindows;
|
||||||
|
storageMode: "supermemory" | "stub";
|
||||||
elapsedMs: number;
|
elapsedMs: number;
|
||||||
}
|
}
|
||||||
| { ok: false; error: string; elapsedMs: number };
|
| { ok: false; error: string; elapsedMs: number };
|
||||||
@@ -34,6 +45,7 @@ export type MonthlyRunResult =
|
|||||||
monthKey: string;
|
monthKey: string;
|
||||||
daysInMonth: number;
|
daysInMonth: number;
|
||||||
schedule: MonthlySchedule;
|
schedule: MonthlySchedule;
|
||||||
|
storageMode: "supermemory" | "stub";
|
||||||
elapsedMs: number;
|
elapsedMs: number;
|
||||||
}
|
}
|
||||||
| { ok: false; error: string; elapsedMs: number };
|
| { ok: false; error: string; elapsedMs: number };
|
||||||
@@ -49,15 +61,23 @@ export async function runDebugScheduleDaily(
|
|||||||
today.getDate() + 1,
|
today.getDate() + 1,
|
||||||
);
|
);
|
||||||
const dateKey = formatDateKey(tomorrow);
|
const dateKey = formatDateKey(tomorrow);
|
||||||
|
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
|
||||||
|
const db = opts.noSupermemory ? new MemoryStub() : undefined;
|
||||||
|
|
||||||
const brain = await Brain.createDebug({ personality: opts.personality });
|
const brain = await Brain.createDebug(
|
||||||
|
{ personality: opts.personality },
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
|
||||||
const scheduleSpinner = ora(
|
const steps = new StepDriver(4);
|
||||||
`Generating daily schedule for ${dateKey}...`,
|
|
||||||
).start();
|
const schedule = await runCreateDailyScheduleSteps(
|
||||||
const schedule = await brain.createDailySchedule(today, opts.message);
|
brain,
|
||||||
|
today,
|
||||||
|
opts.message,
|
||||||
|
steps,
|
||||||
|
);
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
scheduleSpinner.fail("Daily schedule generation failed");
|
|
||||||
const elapsedMs = Date.now() - startTime;
|
const elapsedMs = Date.now() - startTime;
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -65,19 +85,11 @@ export async function runDebugScheduleDaily(
|
|||||||
elapsedMs,
|
elapsedMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
scheduleSpinner.succeed(
|
|
||||||
`Daily schedule generated (${schedule.items.length} slots)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
printSection(
|
steps.start("deriving availability (SCHEDULE_AVAILABILITY)");
|
||||||
`Daily Schedule — ${dateKey} (${tomorrow.toLocaleDateString("en-US", { weekday: "long" })})`,
|
|
||||||
);
|
|
||||||
console.log(JSON.stringify(schedule, null, 2));
|
|
||||||
|
|
||||||
const availSpinner = ora("Deriving availability...").start();
|
|
||||||
const availability = await brain.deriveAvailabilityFromSchedule(schedule);
|
const availability = await brain.deriveAvailabilityFromSchedule(schedule);
|
||||||
if (!availability) {
|
if (!availability) {
|
||||||
availSpinner.fail("Availability derivation failed");
|
steps.fail("see error above");
|
||||||
const elapsedMs = Date.now() - startTime;
|
const elapsedMs = Date.now() - startTime;
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -85,16 +97,29 @@ export async function runDebugScheduleDaily(
|
|||||||
elapsedMs,
|
elapsedMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
availSpinner.succeed(
|
steps.done(`${availability.items.length} windows`);
|
||||||
`Availability derived (${availability.items.length} windows)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
printSection(`Availability — ${dateKey}`);
|
console.log();
|
||||||
|
printSection(`Schedule — daily (${dateKey})`);
|
||||||
|
printKeyValue({
|
||||||
|
dateKey,
|
||||||
|
weekday: tomorrow.toLocaleDateString("en-US", { weekday: "long" }),
|
||||||
|
storage: storageMode,
|
||||||
|
slots: String(schedule.items.length),
|
||||||
|
});
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
printSection(`Step 1/2 output — Daily Schedule (DAILY_SCHEDULE)`);
|
||||||
|
console.log(JSON.stringify(schedule, null, 2));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
printSection(`Step 2/2 output — Availability (SCHEDULE_AVAILABILITY)`);
|
||||||
console.log(JSON.stringify(availability, null, 2));
|
console.log(JSON.stringify(availability, null, 2));
|
||||||
|
console.log();
|
||||||
|
|
||||||
const elapsedMs = Date.now() - startTime;
|
const elapsedMs = Date.now() - startTime;
|
||||||
logger.info(
|
logger.info(
|
||||||
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to disk.`,
|
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -104,6 +129,7 @@ export async function runDebugScheduleDaily(
|
|||||||
tomorrow,
|
tomorrow,
|
||||||
schedule,
|
schedule,
|
||||||
availability,
|
availability,
|
||||||
|
storageMode,
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -115,15 +141,23 @@ export async function runDebugScheduleMonthly(
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
const next = nextMonth(today);
|
const next = nextMonth(today);
|
||||||
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
|
const monthKey = `${next.year}-${pad2(next.month + 1)}`;
|
||||||
|
const storageMode = opts.noSupermemory ? "stub" : "supermemory";
|
||||||
|
const db = opts.noSupermemory ? new MemoryStub() : undefined;
|
||||||
|
|
||||||
const brain = await Brain.createDebug({ personality: opts.personality });
|
const brain = await Brain.createDebug(
|
||||||
|
{ personality: opts.personality },
|
||||||
|
db,
|
||||||
|
);
|
||||||
|
|
||||||
const scheduleSpinner = ora(
|
const steps = new StepDriver(3);
|
||||||
`Generating monthly schedule for ${monthKey} (${next.daysInMonth} days)...`,
|
|
||||||
).start();
|
const schedule = await runCreateMonthlyScheduleSteps(
|
||||||
const schedule = await brain.createMonthlySchedule(today, opts.message);
|
brain,
|
||||||
|
today,
|
||||||
|
opts.message,
|
||||||
|
steps,
|
||||||
|
);
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
scheduleSpinner.fail("Monthly schedule generation failed");
|
|
||||||
const elapsedMs = Date.now() - startTime;
|
const elapsedMs = Date.now() - startTime;
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -131,16 +165,24 @@ export async function runDebugScheduleMonthly(
|
|||||||
elapsedMs,
|
elapsedMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
scheduleSpinner.succeed(
|
|
||||||
`Monthly schedule generated (${schedule.items.length} day summaries)`,
|
|
||||||
);
|
|
||||||
|
|
||||||
printSection(`Monthly Schedule — ${monthKey} (${next.daysInMonth} days)`);
|
console.log();
|
||||||
|
printSection(`Schedule — monthly (${monthKey})`);
|
||||||
|
printKeyValue({
|
||||||
|
monthKey,
|
||||||
|
daysInMonth: String(next.daysInMonth),
|
||||||
|
storage: storageMode,
|
||||||
|
summaries: String(schedule.items.length),
|
||||||
|
});
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
printSection(`Step 1/1 output — Monthly Schedule (MONTHLY_SCHEDULE)`);
|
||||||
console.log(JSON.stringify(schedule, null, 2));
|
console.log(JSON.stringify(schedule, null, 2));
|
||||||
|
console.log();
|
||||||
|
|
||||||
const elapsedMs = Date.now() - startTime;
|
const elapsedMs = Date.now() - startTime;
|
||||||
logger.info(
|
logger.info(
|
||||||
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to disk. (Availability applies per-day and is not generated for the monthly view.)`,
|
`Debug run complete in ${formatDuration(elapsedMs)}. Nothing was written to real disk. (Availability applies per-day and is not generated for the monthly view.)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -149,6 +191,7 @@ export async function runDebugScheduleMonthly(
|
|||||||
monthKey,
|
monthKey,
|
||||||
daysInMonth: next.daysInMonth,
|
daysInMonth: next.daysInMonth,
|
||||||
schedule,
|
schedule,
|
||||||
|
storageMode,
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -158,38 +201,53 @@ export function addScheduleSubcommand(parent: Command): Command {
|
|||||||
.command("schedule")
|
.command("schedule")
|
||||||
.description("Generate a test schedule (no disk writes)");
|
.description("Generate a test schedule (no disk writes)");
|
||||||
|
|
||||||
cmd.command("daily")
|
cmd
|
||||||
|
.command("daily")
|
||||||
.description(
|
.description(
|
||||||
"Generate a daily schedule for tomorrow and print schedule + availability",
|
"Generate a daily schedule for tomorrow and print schedule + availability",
|
||||||
)
|
)
|
||||||
.requiredOption("-m, --message <text>", "User direction for the schedule")
|
.requiredOption("-m, --message <text>", "User direction for the schedule")
|
||||||
.requiredOption("-p, --personality <text>", "Brain personality to use")
|
.requiredOption("-p, --personality <text>", "Brain personality to use")
|
||||||
.action(async (opts: ScheduleOptions) => {
|
.option(
|
||||||
const result = await runDebugScheduleDaily(opts);
|
"--no-supermemory",
|
||||||
|
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
|
||||||
|
)
|
||||||
|
.action(
|
||||||
|
async (opts: { message: string; personality: string; supermemory: boolean }) => {
|
||||||
|
const result = await runDebugScheduleDaily({
|
||||||
|
message: opts.message,
|
||||||
|
personality: opts.personality,
|
||||||
|
noSupermemory: opts.supermemory === false,
|
||||||
|
});
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
logger.error(result.error);
|
logger.error(result.error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
cmd.command("monthly")
|
cmd
|
||||||
|
.command("monthly")
|
||||||
.description("Generate a monthly schedule for next month and print it")
|
.description("Generate a monthly schedule for next month and print it")
|
||||||
.requiredOption("-m, --message <text>", "User direction for the schedule")
|
.requiredOption("-m, --message <text>", "User direction for the schedule")
|
||||||
.requiredOption("-p, --personality <text>", "Brain personality to use")
|
.requiredOption("-p, --personality <text>", "Brain personality to use")
|
||||||
.action(async (opts: ScheduleOptions) => {
|
.option(
|
||||||
const result = await runDebugScheduleMonthly(opts);
|
"--no-supermemory",
|
||||||
|
"Use an in-memory stub instead of the real supermemory API (no network, no API key required)",
|
||||||
|
)
|
||||||
|
.action(
|
||||||
|
async (opts: { message: string; personality: string; supermemory: boolean }) => {
|
||||||
|
const result = await runDebugScheduleMonthly({
|
||||||
|
message: opts.message,
|
||||||
|
personality: opts.personality,
|
||||||
|
noSupermemory: opts.supermemory === false,
|
||||||
|
});
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
logger.error(result.error);
|
logger.error(result.error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
function printSection(title: string): void {
|
|
||||||
const line = "─".repeat(Math.max(40, title.length + 4));
|
|
||||||
console.log(`\n┌${line}┐`);
|
|
||||||
console.log(`│ ${title}`);
|
|
||||||
console.log(`└${line}┘`);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import { join } from "path";
|
|||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
openrouterApiKey: string;
|
openrouterApiKey: string;
|
||||||
dbPath: string;
|
supermemoryApiKey: string;
|
||||||
braindbPath: string;
|
braindbPath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const openrouterApiKey = process.env["OPENROUTER_API_KEY"];
|
const openrouterApiKey = process.env["OPENROUTER_API_KEY"];
|
||||||
if (!openrouterApiKey) throw new Error("OPENROUTER_API_KEY is missing");
|
if (!openrouterApiKey) throw new Error("OPENROUTER_API_KEY is missing");
|
||||||
const dbPath = join(process.cwd(), process.env["DB_PATH"] ?? "brainbox.db");
|
|
||||||
|
const supermemoryApiKey = process.env["SUPERMEMORY_API_KEY"];
|
||||||
|
if (!supermemoryApiKey) throw new Error("SUPERMEMORY_API_KEY is missing");
|
||||||
|
|
||||||
const braindbPath = join(
|
const braindbPath = join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
process.env["BRAINDB_PATH"] ?? "brainbox.json",
|
process.env["BRAINDB_PATH"] ?? "brainbox.json",
|
||||||
@@ -17,6 +20,6 @@ const braindbPath = join(
|
|||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
openrouterApiKey,
|
openrouterApiKey,
|
||||||
dbPath,
|
supermemoryApiKey,
|
||||||
braindbPath,
|
braindbPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { config } from "@/config";
|
|
||||||
import { OpenRouter } from "@openrouter/sdk";
|
|
||||||
import type { EmbeddingProvider } from "identitydb";
|
|
||||||
|
|
||||||
export const QWEN_EMBEDDING_MODEL = "qwen/qwen3-embedding-8b" as const;
|
|
||||||
export const QWEN_EMBEDDING_DIMENSIONS = 512 as const;
|
|
||||||
|
|
||||||
export class OpenRouterEmbeddingProvider implements EmbeddingProvider {
|
|
||||||
readonly model: string = QWEN_EMBEDDING_MODEL;
|
|
||||||
readonly dimensions: number = QWEN_EMBEDDING_DIMENSIONS;
|
|
||||||
|
|
||||||
private client: OpenRouter;
|
|
||||||
|
|
||||||
constructor(apiKey: string = config.openrouterApiKey) {
|
|
||||||
this.client = new OpenRouter({ apiKey, appTitle: "boxbrain" });
|
|
||||||
}
|
|
||||||
|
|
||||||
async embed(input: string): Promise<number[]> {
|
|
||||||
const result = await this.embedBatch([input]);
|
|
||||||
return result[0]!;
|
|
||||||
}
|
|
||||||
|
|
||||||
async embedMany(inputs: string[]): Promise<number[][]> {
|
|
||||||
if (inputs.length === 0) return [];
|
|
||||||
return await this.embedBatch(inputs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async embedBatch(inputs: string[]): Promise<number[][]> {
|
|
||||||
const response = await this.client.embeddings.generate({
|
|
||||||
requestBody: {
|
|
||||||
model: this.model,
|
|
||||||
input: inputs,
|
|
||||||
dimensions: this.dimensions,
|
|
||||||
encodingFormat: "float",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (typeof response === "string") {
|
|
||||||
throw new Error("OpenRouter returned a non-JSON embeddings response");
|
|
||||||
}
|
|
||||||
const ordered = new Array<number[]>(inputs.length);
|
|
||||||
for (const item of response.data) {
|
|
||||||
if (typeof item.embedding === "string") {
|
|
||||||
throw new Error(
|
|
||||||
"OpenRouter returned a base64 embedding but float was requested",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const index = item.index ?? 0;
|
|
||||||
ordered[index] = item.embedding;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < ordered.length; i += 1) {
|
|
||||||
if (!ordered[i]) {
|
|
||||||
throw new Error(`OpenRouter omitted embedding for input index ${i}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ordered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +1,3 @@
|
|||||||
export const extractedFactSchema = {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
items: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
statement: { type: "string" },
|
|
||||||
summary: { type: "string" },
|
|
||||||
source: { type: "string" },
|
|
||||||
confidence: { type: "number" },
|
|
||||||
metadata: { type: "object", additionalProperties: false },
|
|
||||||
topics: {
|
|
||||||
type: "array",
|
|
||||||
items: {
|
|
||||||
type: "object",
|
|
||||||
additionalProperties: false,
|
|
||||||
properties: {
|
|
||||||
name: { type: "string" },
|
|
||||||
category: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["entity", "concept", "temporal", "custom"],
|
|
||||||
},
|
|
||||||
granularity: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["abstract", "concrete", "mixed"],
|
|
||||||
},
|
|
||||||
role: { type: "string" },
|
|
||||||
description: { type: "string" },
|
|
||||||
metadata: { type: "object", additionalProperties: false },
|
|
||||||
},
|
|
||||||
required: [
|
|
||||||
"name",
|
|
||||||
"category",
|
|
||||||
"granularity",
|
|
||||||
"role",
|
|
||||||
"description",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["statement", "summary", "source", "confidence", "topics"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["items"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const timeString = {
|
const timeString = {
|
||||||
type: "string",
|
type: "string",
|
||||||
pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
pattern: "^([01][0-9]|2[0-3]):[0-5][0-9]$",
|
||||||
@@ -133,9 +82,6 @@ export const availabilitySchema = {
|
|||||||
// Types — co-located with their schemas.
|
// Types — co-located with their schemas.
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
import type { ExtractedFact } from "identitydb";
|
|
||||||
|
|
||||||
/** A single 30-minute slot in a daily schedule. Matches `dailyScheduleSchema.items.items`. */
|
|
||||||
export type DailySlot = {
|
export type DailySlot = {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
@@ -143,52 +89,27 @@ export type DailySlot = {
|
|||||||
notes: string;
|
notes: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A complete daily schedule: a wrapped object containing exactly 48 half-hour
|
|
||||||
* slots. Matches `dailyScheduleSchema` (the LLM is constrained to return the
|
|
||||||
* `{ items: [...] }` envelope).
|
|
||||||
*/
|
|
||||||
export type DailySchedule = {
|
export type DailySchedule = {
|
||||||
items: DailySlot[];
|
items: DailySlot[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A single day's summary inside a monthly schedule. Matches `monthlyScheduleSchema.items.items`. */
|
|
||||||
export type MonthlyDay = {
|
export type MonthlyDay = {
|
||||||
day: number;
|
day: number;
|
||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A complete monthly schedule: a wrapped object containing one entry per day
|
|
||||||
* of the month. Matches `monthlyScheduleSchema`.
|
|
||||||
*/
|
|
||||||
export type MonthlySchedule = {
|
export type MonthlySchedule = {
|
||||||
items: MonthlyDay[];
|
items: MonthlyDay[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Reachability status for a single availability window. */
|
|
||||||
export type AvailabilityStatus = "online" | "do-not-disturb" | "offline";
|
export type AvailabilityStatus = "online" | "do-not-disturb" | "offline";
|
||||||
|
|
||||||
/** A single availability window. Matches `availabilitySchema.items.items`. */
|
|
||||||
export type Availability = {
|
export type Availability = {
|
||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
status: AvailabilityStatus;
|
status: AvailabilityStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The full set of availability windows for a day: a wrapped object containing
|
|
||||||
* one or more windows. Matches `availabilitySchema`.
|
|
||||||
*/
|
|
||||||
export type AvailabilityWindows = {
|
export type AvailabilityWindows = {
|
||||||
items: Availability[];
|
items: Availability[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The wrapped envelope the LLM returns for `extractedFactSchema`. The inner
|
|
||||||
* `items` array is the list of `ExtractedFact` (which is defined in the
|
|
||||||
* external `identitydb` package).
|
|
||||||
*/
|
|
||||||
export type ExtractedFactResult = {
|
|
||||||
items: ExtractedFact[];
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user