This commit is contained in:
24
.gitea/workflows/ci.yml
Normal file
24
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: oven/bun:1
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
- name: Test
|
||||||
|
run: bun run test
|
||||||
|
- name: Typecheck
|
||||||
|
run: bun run check
|
||||||
|
- name: Build
|
||||||
|
run: bun run build
|
||||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.log
|
||||||
|
.data/
|
||||||
|
.hermes/
|
||||||
163
README.md
Normal file
163
README.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# BoxBrain
|
||||||
|
|
||||||
|
BoxBrain is a TypeScript framework for designing LLM harnesses that make a persona feel like a real person, backed by IdentityDB memory spaces.
|
||||||
|
|
||||||
|
This repository was reset from scratch. The current implementation focuses on a clean framework core rather than a concrete chatbot product.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
BoxBrain helps API users build an LLM-driven persona with:
|
||||||
|
|
||||||
|
- persona initialization into an isolated IdentityDB-backed space
|
||||||
|
- realistic schedule generation
|
||||||
|
- schedule-derived availability (`online`, `do-not-disturb`, `offline`)
|
||||||
|
- reply and proactive conversation APIs
|
||||||
|
- sleep-time memory extraction into durable facts
|
||||||
|
- debug hooks that expose the framework flow and persona reasoning pipeline
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core API
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Persona, createSqliteIdentityMemoryStore } from 'boxbrain';
|
||||||
|
|
||||||
|
const memory = await createSqliteIdentityMemoryStore('.data/mina.sqlite');
|
||||||
|
|
||||||
|
const persona = new Persona(
|
||||||
|
'Mina',
|
||||||
|
'Mina is a careful student who likes quiet cafes and is preparing for exams.',
|
||||||
|
{
|
||||||
|
memory,
|
||||||
|
models: {
|
||||||
|
conversation: yourConversationModel,
|
||||||
|
memoryExtraction: yourMemoryExtractionModel,
|
||||||
|
},
|
||||||
|
debug: (event) => console.log(event),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const space = await persona.ready();
|
||||||
|
```
|
||||||
|
|
||||||
|
A persona can also be loaded from an existing space:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const persona = new Persona(space.id, { memory, models });
|
||||||
|
await persona.ready();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Persona initialization
|
||||||
|
|
||||||
|
`new Persona(displayName, message, options)` creates a new isolated persona space. The seed message is the single freeform place for personality, history, likes, dislikes, relationships, and other facts about the persona.
|
||||||
|
|
||||||
|
`new Persona(spaceId, options)` loads an existing persona space.
|
||||||
|
|
||||||
|
If `models.initialization` is provided, BoxBrain asks it for initial facts. If no initialization model is provided, BoxBrain stores a minimal seed fact about the persona. If the model intentionally returns an empty list, no fallback fact is stored.
|
||||||
|
|
||||||
|
## Schedule API
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await persona.createDailySchedule(now, 'Keep a normal work day.');
|
||||||
|
await persona.createMonthlySchedule(now, 'Mostly study, with occasional rest.');
|
||||||
|
await persona.deleteSchedulesBefore(cutoff);
|
||||||
|
await persona.deleteSchedulesOlderThan(cutoff);
|
||||||
|
```
|
||||||
|
|
||||||
|
`createDailySchedule(datetime, message)` creates tomorrow's schedule in 10-minute blocks. For example, if `datetime` is May 1, the generated daily schedule covers May 2 00:00 through May 3 00:00.
|
||||||
|
|
||||||
|
`createMonthlySchedule(datetime, message)` creates day-level schedule outlines for the next 30 days.
|
||||||
|
|
||||||
|
Schedules are stored through the configured memory store. The IdentityDB store records schedule entries as facts under schedule-related topics.
|
||||||
|
|
||||||
|
## Availability API
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const availability = await persona.getTodayScheduledAvailability(now);
|
||||||
|
```
|
||||||
|
|
||||||
|
Availability is derived from schedule entries and kept in memory rather than persisted separately. The window covers today 00:00 through tomorrow 24:00. When the date changes, BoxBrain rebuilds the snapshot from schedule entries in memory.
|
||||||
|
|
||||||
|
Schedule activities map to availability roughly as:
|
||||||
|
|
||||||
|
- `sleep` → `offline`
|
||||||
|
- `work`, `study`, `job-search`, `travel`, `commute` → `do-not-disturb`
|
||||||
|
- rest, meals, exercise, errands, social time, free time → `online`
|
||||||
|
|
||||||
|
## Conversation API
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const reply = await persona.sendMessage({
|
||||||
|
datetime: now,
|
||||||
|
messageHistory: [
|
||||||
|
{ sender: 'persona', time: yesterday, content: 'See you later.' },
|
||||||
|
{ sender: 'user', time: now, content: 'What are you doing?' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const opener = await persona.startConversation({
|
||||||
|
datetime: now,
|
||||||
|
messageHistory: [],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Before generating a reply, BoxBrain always loads mandatory context:
|
||||||
|
|
||||||
|
- formatted yesterday/today message history supplied by the API user
|
||||||
|
- yesterday, today, and tomorrow schedule entries
|
||||||
|
- current schedule-derived availability
|
||||||
|
- IdentityDB facts related to the persona and the user
|
||||||
|
|
||||||
|
If no relevant mandatory memory is found, the model context explicitly says `기억이 없음` so the persona can react naturally instead of pretending to remember.
|
||||||
|
|
||||||
|
Conversation models return one or more outgoing messages. The framework instruction tells the model to behave like a `send_message` tool and, unless the persona prefers otherwise, keep each message to at most one sentence.
|
||||||
|
|
||||||
|
If `getLatestMessageHistory` and `models.rewrite` are provided, BoxBrain can detect messages that arrived while a draft was being generated and ask the rewrite model whether to discard and regenerate the stale draft.
|
||||||
|
|
||||||
|
## sleepMemory
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await persona.sleepMemory({
|
||||||
|
datetime: '2026-05-02T00:00:00.000Z',
|
||||||
|
messageHistory: messagesFromMay1,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`sleepMemory` asks `models.memoryExtraction` to inspect the provided message history, objectivize durable facts, and persist them through the memory store. The recommended cadence is daily around midnight, passing the previous day's messages.
|
||||||
|
|
||||||
|
## Debug hooks
|
||||||
|
|
||||||
|
Every major pipeline step can emit a debug event:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const persona = new Persona('Mina', seed, {
|
||||||
|
debug(event) {
|
||||||
|
// Write to a file, messenger, trace UI, etc.
|
||||||
|
console.log(event.name, event.data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples include:
|
||||||
|
|
||||||
|
- `persona.initialized`
|
||||||
|
- `persona.loaded`
|
||||||
|
- `persona.schedule.daily.generated`
|
||||||
|
- `persona.availability.refreshed`
|
||||||
|
- `persona.conversation.context.loaded`
|
||||||
|
- `persona.conversation.rewrite.checked`
|
||||||
|
- `persona.memory.sleep.persisted`
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run test
|
||||||
|
bun run check
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The current test suite covers persona creation/loading, schedule generation/pruning, availability derivation, conversation context assembly, stale-draft rewrite checks, proactive conversation starts, and sleep-memory persistence.
|
||||||
412
bun.lock
Normal file
412
bun.lock
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "boxbrain",
|
||||||
|
"dependencies": {
|
||||||
|
"identitydb": "0.2.1",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"tsup": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"vitest": "latest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"esbuild",
|
||||||
|
],
|
||||||
|
"packages": {
|
||||||
|
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||||
|
|
||||||
|
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
|
||||||
|
|
||||||
|
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
|
||||||
|
|
||||||
|
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
|
||||||
|
|
||||||
|
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
|
||||||
|
|
||||||
|
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
|
||||||
|
|
||||||
|
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
|
||||||
|
|
||||||
|
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
|
||||||
|
|
||||||
|
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
|
||||||
|
|
||||||
|
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
|
||||||
|
|
||||||
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
|
||||||
|
|
||||||
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||||
|
|
||||||
|
"@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0", "", { "os": "linux", "cpu": "arm" }, "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow=="],
|
||||||
|
|
||||||
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg=="],
|
||||||
|
|
||||||
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0", "", {}, "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.3", "", { "os": "android", "cpu": "arm" }, "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.3", "", { "os": "android", "cpu": "arm64" }, "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.3", "", { "os": "linux", "cpu": "arm" }, "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.3", "", { "os": "linux", "cpu": "none" }, "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.3", "", { "os": "none", "cpu": "arm64" }, "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A=="],
|
||||||
|
|
||||||
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.3", "", { "os": "win32", "cpu": "x64" }, "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA=="],
|
||||||
|
|
||||||
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||||
|
|
||||||
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.7.0", "", { "dependencies": { "undici-types": "~7.21.0" } }, "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg=="],
|
||||||
|
|
||||||
|
"@vitest/expect": ["@vitest/expect@4.1.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg=="],
|
||||||
|
|
||||||
|
"@vitest/mocker": ["@vitest/mocker@4.1.6", "", { "dependencies": { "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ=="],
|
||||||
|
|
||||||
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.6", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw=="],
|
||||||
|
|
||||||
|
"@vitest/runner": ["@vitest/runner@4.1.6", "", { "dependencies": { "@vitest/utils": "4.1.6", "pathe": "^2.0.3" } }, "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA=="],
|
||||||
|
|
||||||
|
"@vitest/snapshot": ["@vitest/snapshot@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw=="],
|
||||||
|
|
||||||
|
"@vitest/spy": ["@vitest/spy@4.1.6", "", {}, "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg=="],
|
||||||
|
|
||||||
|
"@vitest/utils": ["@vitest/utils@4.1.6", "", { "dependencies": { "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
|
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||||
|
|
||||||
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
||||||
|
|
||||||
|
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||||
|
|
||||||
|
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||||
|
|
||||||
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||||
|
|
||||||
|
"confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||||
|
|
||||||
|
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||||
|
|
||||||
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"es-module-lexer": ["es-module-lexer@2.1.0", "", {}, "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="],
|
||||||
|
|
||||||
|
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
|
||||||
|
|
||||||
|
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||||
|
|
||||||
|
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
|
||||||
|
|
||||||
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
|
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"identitydb": ["identitydb@0.2.1", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-e+caNqI7F6JaqgyIFQbdiT5/2Frs5PEJgy3mmF+qUVspHZ4z6QtFF5jonDnpYtJpZ9guPWfeQ/xteeaiwOJ5zA=="],
|
||||||
|
|
||||||
|
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||||
|
|
||||||
|
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.17", "", {}, "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q=="],
|
||||||
|
|
||||||
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
|
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||||
|
|
||||||
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
|
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
|
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"mysql2": ["mysql2@3.22.3", "", { "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-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA=="],
|
||||||
|
|
||||||
|
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||||
|
|
||||||
|
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||||
|
|
||||||
|
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
|
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||||
|
|
||||||
|
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
|
||||||
|
|
||||||
|
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
|
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||||
|
|
||||||
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
|
||||||
|
"rolldown": ["rolldown@1.0.0", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@rolldown/pluginutils": "1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0", "@rolldown/binding-darwin-arm64": "1.0.0", "@rolldown/binding-darwin-x64": "1.0.0", "@rolldown/binding-freebsd-x64": "1.0.0", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", "@rolldown/binding-linux-arm64-gnu": "1.0.0", "@rolldown/binding-linux-arm64-musl": "1.0.0", "@rolldown/binding-linux-ppc64-gnu": "1.0.0", "@rolldown/binding-linux-s390x-gnu": "1.0.0", "@rolldown/binding-linux-x64-gnu": "1.0.0", "@rolldown/binding-linux-x64-musl": "1.0.0", "@rolldown/binding-openharmony-arm64": "1.0.0", "@rolldown/binding-wasm32-wasi": "1.0.0", "@rolldown/binding-win32-arm64-msvc": "1.0.0", "@rolldown/binding-win32-x64-msvc": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA=="],
|
||||||
|
|
||||||
|
"rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||||
|
|
||||||
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
|
"sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
|
||||||
|
|
||||||
|
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
|
|
||||||
|
"std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="],
|
||||||
|
|
||||||
|
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
|
||||||
|
|
||||||
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
|
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||||
|
|
||||||
|
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
|
|
||||||
|
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||||
|
|
||||||
|
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||||
|
|
||||||
|
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||||
|
|
||||||
|
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|
||||||
|
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="],
|
||||||
|
|
||||||
|
"vite": ["vite@8.0.12", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg=="],
|
||||||
|
|
||||||
|
"vitest": ["vitest@4.1.6", "", { "dependencies": { "@vitest/expect": "4.1.6", "@vitest/mocker": "4.1.6", "@vitest/pretty-format": "4.1.6", "@vitest/runner": "4.1.6", "@vitest/snapshot": "4.1.6", "@vitest/spy": "4.1.6", "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.6", "@vitest/browser-preview": "4.1.6", "@vitest/browser-webdriverio": "4.1.6", "@vitest/coverage-istanbul": "4.1.6", "@vitest/coverage-v8": "4.1.6", "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ=="],
|
||||||
|
|
||||||
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"vitest/tinyexec": ["tinyexec@1.1.2", "", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "boxbrain",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Human-like persona harness framework powered by LLMs and IdentityDB.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "vitest run",
|
||||||
|
"check": "tsc --noEmit",
|
||||||
|
"build": "tsup src/index.ts --format esm --dts --sourcemap --clean"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"identitydb": "0.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
"tsup": "latest",
|
||||||
|
"typescript": "latest",
|
||||||
|
"vitest": "latest"
|
||||||
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/conversation.ts
Normal file
64
src/conversation.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import type {
|
||||||
|
BoxBrainMemoryStore,
|
||||||
|
DateTimeInput,
|
||||||
|
MandatoryConversationContext,
|
||||||
|
MemorySpace,
|
||||||
|
PersonaMessage,
|
||||||
|
ScheduledAvailabilitySnapshot,
|
||||||
|
} from './types';
|
||||||
|
import { addUtcDays, buildAvailabilitySnapshot, dateKeysAround, startOfUtcDay, toIso } from './schedule';
|
||||||
|
|
||||||
|
export function formatMessageHistory(input: { personaName: string; messages: PersonaMessage[] }): string {
|
||||||
|
return input.messages
|
||||||
|
.map((message) => {
|
||||||
|
const sender = message.sender === 'persona' ? input.personaName : 'user';
|
||||||
|
return `${sender}@${toIso(message.time)}: ${message.content}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function conversationInstruction(): string {
|
||||||
|
return [
|
||||||
|
'You are controlling the persona, not a generic assistant.',
|
||||||
|
'Use the send_message tool conceptually: return one or more outgoing messages.',
|
||||||
|
'Unless the persona strongly prefers otherwise, keep each outgoing message to at most one sentence.',
|
||||||
|
'Prefer short, natural, chat-like wording and allow splitting one thought into multiple messages.',
|
||||||
|
'If mandatory memory says "기억이 없음", the persona may naturally wonder about missing context instead of pretending to remember.',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function memoryExtractionInstruction(now: string): string {
|
||||||
|
return [
|
||||||
|
`Current objective time: ${now}.`,
|
||||||
|
'Read the message history and extract durable facts worth remembering.',
|
||||||
|
'Objectivize subjective statements before storage.',
|
||||||
|
'Example: "I started TypeScript in 2025" becomes "The user started TypeScript in 2025."',
|
||||||
|
'Prefer facts about the persona, the user, their relationship, preferences, history, schedule-relevant events, and stable traits.',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildMandatoryConversationContext(input: {
|
||||||
|
persona: MemorySpace;
|
||||||
|
now: DateTimeInput;
|
||||||
|
memory: BoxBrainMemoryStore;
|
||||||
|
messages: PersonaMessage[];
|
||||||
|
availability: ScheduledAvailabilitySnapshot;
|
||||||
|
}): Promise<MandatoryConversationContext> {
|
||||||
|
const now = startOfUtcDay(input.now);
|
||||||
|
const from = addUtcDays(now, -1).toISOString();
|
||||||
|
const to = addUtcDays(now, 2).toISOString();
|
||||||
|
const scheduleEntries = await input.memory.listScheduleEntries(input.persona.id, from, to);
|
||||||
|
const personaAndUserFacts = await input.memory.findFacts(input.persona.id, ['persona', input.persona.displayName, 'user']);
|
||||||
|
const memorySummary = personaAndUserFacts.length === 0
|
||||||
|
? '기억이 없음'
|
||||||
|
: personaAndUserFacts.map((fact) => `- ${fact.statement}`).join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
formattedMessageHistory: formatMessageHistory({ personaName: input.persona.displayName, messages: input.messages }),
|
||||||
|
conversationWindowLabel: `Required conversation window: yesterday/today. Schedule dates: ${dateKeysAround(input.now).join(', ')}.`,
|
||||||
|
memorySummary,
|
||||||
|
personaAndUserFacts,
|
||||||
|
scheduleEntries,
|
||||||
|
availability: input.availability,
|
||||||
|
};
|
||||||
|
}
|
||||||
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './memory';
|
||||||
|
export * from './schedule';
|
||||||
|
export * from './conversation';
|
||||||
|
export * from './persona';
|
||||||
178
src/memory.ts
Normal file
178
src/memory.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { IdentityDB, type Fact as IdentityFact } from 'identitydb';
|
||||||
|
import type { BoxBrainMemoryStore, FactDraft, MemorySpace, ScheduleEntry, StoredFact } from './types';
|
||||||
|
|
||||||
|
function normalizeTopics(topics: string[]): string[] {
|
||||||
|
return [...new Set(topics.map((topic) => topic.trim()).filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesAnyTopic(fact: StoredFact, topics: string[]): boolean {
|
||||||
|
const normalized = new Set(normalizeTopics(topics).map((topic) => topic.toLowerCase()));
|
||||||
|
return fact.topics.some((topic) => normalized.has(topic.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InMemoryMemoryStore implements BoxBrainMemoryStore {
|
||||||
|
readonly spaces = new Map<string, MemorySpace>();
|
||||||
|
readonly facts = new Map<string, StoredFact[]>();
|
||||||
|
readonly schedules = new Map<string, ScheduleEntry[]>();
|
||||||
|
|
||||||
|
async createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise<MemorySpace> {
|
||||||
|
const slug = input.displayName.toLowerCase().replace(/[^a-z0-9가-힣]+/gi, '-').replace(/^-|-$/g, '') || 'persona';
|
||||||
|
const space: MemorySpace = {
|
||||||
|
id: `persona-${slug}-${crypto.randomUUID()}`,
|
||||||
|
displayName: input.displayName,
|
||||||
|
createdAt: input.now,
|
||||||
|
metadata: { seedMessage: input.seedMessage },
|
||||||
|
};
|
||||||
|
this.spaces.set(space.id, space);
|
||||||
|
return space;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpace(spaceId: string): Promise<MemorySpace | null> {
|
||||||
|
return this.spaces.get(spaceId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFact(spaceId: string, fact: FactDraft): Promise<StoredFact> {
|
||||||
|
const stored: StoredFact = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
statement: fact.statement,
|
||||||
|
topics: normalizeTopics(fact.topics),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
...(fact.confidence === undefined ? {} : { confidence: fact.confidence }),
|
||||||
|
...(fact.source === undefined ? {} : { source: fact.source }),
|
||||||
|
...(fact.metadata === undefined ? {} : { metadata: fact.metadata }),
|
||||||
|
};
|
||||||
|
const existing = this.facts.get(spaceId) ?? [];
|
||||||
|
existing.push(stored);
|
||||||
|
this.facts.set(spaceId, existing);
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findFacts(spaceId: string, topics: string[]): Promise<StoredFact[]> {
|
||||||
|
const facts = this.facts.get(spaceId) ?? [];
|
||||||
|
if (topics.length === 0) return [...facts];
|
||||||
|
return facts.filter((fact) => includesAnyTopic(fact, topics));
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise<void> {
|
||||||
|
const existing = (this.schedules.get(spaceId) ?? []).filter((entry) => !entries.some((incoming) => incoming.id === entry.id));
|
||||||
|
this.schedules.set(spaceId, [...existing, ...entries].sort((a, b) => a.startAt.localeCompare(b.startAt)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise<ScheduleEntry[]> {
|
||||||
|
return (this.schedules.get(spaceId) ?? [])
|
||||||
|
.filter((entry) => entry.startAt < toExclusive && entry.endAt > fromInclusive)
|
||||||
|
.sort((a, b) => a.startAt.localeCompare(b.startAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteScheduleEntriesBefore(spaceId: string, cutoffExclusive: string): Promise<number> {
|
||||||
|
const entries = this.schedules.get(spaceId) ?? [];
|
||||||
|
const kept = entries.filter((entry) => entry.endAt > cutoffExclusive);
|
||||||
|
this.schedules.set(spaceId, kept);
|
||||||
|
return entries.length - kept.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IdentityDbMemoryStoreOptions {
|
||||||
|
db: IdentityDB;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IdentityDbMemoryStore implements BoxBrainMemoryStore {
|
||||||
|
constructor(private readonly options: IdentityDbMemoryStoreOptions) {}
|
||||||
|
|
||||||
|
async createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise<MemorySpace> {
|
||||||
|
const slug = input.displayName.toLowerCase().replace(/[^a-z0-9가-힣]+/gi, '-').replace(/^-|-$/g, '') || 'persona';
|
||||||
|
const spaceName = `persona-${slug}-${crypto.randomUUID()}`;
|
||||||
|
const space = await this.options.db.upsertSpace({
|
||||||
|
name: spaceName,
|
||||||
|
description: `BoxBrain persona space for ${input.displayName}`,
|
||||||
|
metadata: {
|
||||||
|
boxbrainType: 'persona-space',
|
||||||
|
displayName: input.displayName,
|
||||||
|
seedMessage: input.seedMessage,
|
||||||
|
createdAt: input.now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
id: space.name,
|
||||||
|
displayName: input.displayName,
|
||||||
|
createdAt: space.createdAt,
|
||||||
|
metadata: { seedMessage: input.seedMessage, identityDbSpaceId: space.id },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSpace(spaceId: string): Promise<MemorySpace | null> {
|
||||||
|
const space = await this.options.db.getSpaceByName(spaceId);
|
||||||
|
if (!space) return null;
|
||||||
|
const metadata = typeof space.metadata === 'object' && space.metadata !== null && !Array.isArray(space.metadata) ? space.metadata as Record<string, unknown> : {};
|
||||||
|
const displayName = typeof metadata['displayName'] === 'string' ? metadata['displayName'] : space.name;
|
||||||
|
return { id: space.name, displayName, createdAt: space.createdAt, metadata };
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFact(spaceId: string, fact: FactDraft): Promise<StoredFact> {
|
||||||
|
const stored = await this.options.db.addFact({
|
||||||
|
spaceName: spaceId,
|
||||||
|
statement: fact.statement,
|
||||||
|
confidence: fact.confidence ?? null,
|
||||||
|
source: fact.source ?? null,
|
||||||
|
metadata: (fact.metadata ?? null) as never,
|
||||||
|
topics: normalizeTopics(fact.topics).map((topic) => ({ name: topic, category: 'entity' as const, granularity: 'concrete' as const })),
|
||||||
|
});
|
||||||
|
return this.fromIdentityFact(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findFacts(spaceId: string, topics: string[]): Promise<StoredFact[]> {
|
||||||
|
const uniqueTopics = normalizeTopics(topics);
|
||||||
|
const collected = new Map<string, StoredFact>();
|
||||||
|
for (const topic of uniqueTopics) {
|
||||||
|
const facts = await this.options.db.getTopicFacts(topic, { spaceName: spaceId });
|
||||||
|
for (const fact of facts) {
|
||||||
|
collected.set(fact.id, this.fromIdentityFact(fact));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...collected.values()].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise<void> {
|
||||||
|
for (const entry of entries) {
|
||||||
|
await this.addFact(spaceId, {
|
||||||
|
statement: `${entry.title} from ${entry.startAt} to ${entry.endAt}.`,
|
||||||
|
topics: ['schedule', entry.startAt.slice(0, 10), entry.activity, 'persona'],
|
||||||
|
source: 'boxbrain.schedule',
|
||||||
|
metadata: { ...entry.metadata, scheduleEntry: entry },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise<ScheduleEntry[]> {
|
||||||
|
const facts = await this.findFacts(spaceId, ['schedule']);
|
||||||
|
return facts
|
||||||
|
.map((fact) => fact.metadata?.['scheduleEntry'])
|
||||||
|
.filter((value): value is ScheduleEntry => typeof value === 'object' && value !== null && !Array.isArray(value))
|
||||||
|
.filter((entry) => entry.startAt < toExclusive && entry.endAt > fromInclusive)
|
||||||
|
.sort((a, b) => a.startAt.localeCompare(b.startAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteScheduleEntriesBefore(_spaceId: string, _cutoffExclusive: string): Promise<number> {
|
||||||
|
// IdentityDB is append-oriented at the public API level. Record schedule deletion as a fact at the Persona layer.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private fromIdentityFact(fact: IdentityFact): StoredFact {
|
||||||
|
const metadata = typeof fact.metadata === 'object' && fact.metadata !== null && !Array.isArray(fact.metadata) ? fact.metadata as Record<string, unknown> : undefined;
|
||||||
|
return {
|
||||||
|
id: fact.id,
|
||||||
|
statement: fact.statement,
|
||||||
|
topics: fact.topics.map((topic) => topic.name),
|
||||||
|
createdAt: fact.createdAt,
|
||||||
|
...(fact.confidence === null ? {} : { confidence: fact.confidence }),
|
||||||
|
...(fact.source === null ? {} : { source: fact.source }),
|
||||||
|
...(metadata === undefined ? {} : { metadata }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSqliteIdentityMemoryStore(filename: string): Promise<IdentityDbMemoryStore> {
|
||||||
|
const db = await IdentityDB.connect({ client: 'sqlite', filename });
|
||||||
|
await db.initialize();
|
||||||
|
return new IdentityDbMemoryStore({ db });
|
||||||
|
}
|
||||||
306
src/persona.ts
Normal file
306
src/persona.ts
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
import { InMemoryMemoryStore } from './memory';
|
||||||
|
import {
|
||||||
|
addUtcDays,
|
||||||
|
buildAvailabilitySnapshot,
|
||||||
|
createMonthlyScheduleEntries,
|
||||||
|
createTenMinuteDailySchedule,
|
||||||
|
scheduleTargetDay,
|
||||||
|
startOfUtcDay,
|
||||||
|
toIso,
|
||||||
|
} from './schedule';
|
||||||
|
import {
|
||||||
|
buildMandatoryConversationContext,
|
||||||
|
conversationInstruction,
|
||||||
|
formatMessageHistory,
|
||||||
|
memoryExtractionInstruction,
|
||||||
|
} from './conversation';
|
||||||
|
import type {
|
||||||
|
BoxBrainMemoryStore,
|
||||||
|
DateTimeInput,
|
||||||
|
DebugEvent,
|
||||||
|
FactDraft,
|
||||||
|
MemorySpace,
|
||||||
|
OutgoingMessageDraft,
|
||||||
|
PersonaMessage,
|
||||||
|
PersonaOptions,
|
||||||
|
ScheduleEntry,
|
||||||
|
ScheduledAvailabilitySnapshot,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
interface CreateMode {
|
||||||
|
type: 'create';
|
||||||
|
displayName: string;
|
||||||
|
seedMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoadMode {
|
||||||
|
type: 'load';
|
||||||
|
spaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mode = CreateMode | LoadMode;
|
||||||
|
|
||||||
|
function defaultInitialFact(displayName: string, seedMessage: string): FactDraft {
|
||||||
|
return {
|
||||||
|
statement: `${displayName} is a BoxBrain persona initialized from this seed: ${seedMessage}`,
|
||||||
|
topics: ['persona', displayName],
|
||||||
|
source: 'boxbrain.persona.initialization',
|
||||||
|
confidence: 1,
|
||||||
|
metadata: { boxbrainType: 'persona-initial-fact' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDraft(draft: OutgoingMessageDraft): OutgoingMessageDraft {
|
||||||
|
return {
|
||||||
|
messages: draft.messages.map((message) => message.trim()).filter(Boolean),
|
||||||
|
...(draft.reasoning === undefined ? {} : { reasoning: draft.reasoning }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Persona {
|
||||||
|
private readonly memory: BoxBrainMemoryStore;
|
||||||
|
private readonly options: PersonaOptions;
|
||||||
|
private readonly mode: Mode;
|
||||||
|
private readonly readyPromise: Promise<MemorySpace>;
|
||||||
|
private availabilitySnapshot?: ScheduledAvailabilitySnapshot;
|
||||||
|
|
||||||
|
constructor(displayName: string, seedMessage: string, options?: PersonaOptions);
|
||||||
|
constructor(spaceId: string, options?: PersonaOptions);
|
||||||
|
constructor(first: string, second?: string | PersonaOptions, third?: PersonaOptions) {
|
||||||
|
if (typeof second === 'string') {
|
||||||
|
this.mode = { type: 'create', displayName: first, seedMessage: second };
|
||||||
|
this.options = third ?? {};
|
||||||
|
} else {
|
||||||
|
this.mode = { type: 'load', spaceId: first };
|
||||||
|
this.options = second ?? {};
|
||||||
|
}
|
||||||
|
this.memory = this.options.memory ?? new InMemoryMemoryStore();
|
||||||
|
this.readyPromise = this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ready(): Promise<MemorySpace> {
|
||||||
|
return this.readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDailySchedule(datetime: DateTimeInput, message: string): Promise<ScheduleEntry[]> {
|
||||||
|
const persona = await this.ready();
|
||||||
|
const targetDay = scheduleTargetDay(datetime);
|
||||||
|
const entries = createTenMinuteDailySchedule({ persona, targetDay, message });
|
||||||
|
await this.emit('persona.schedule.daily.generated', { targetDay: targetDay.toISOString(), count: entries.length, message });
|
||||||
|
await this.memory.saveScheduleEntries(persona.id, entries);
|
||||||
|
await this.refreshAvailability(datetime);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMonthlySchedule(datetime: DateTimeInput, message: string): Promise<ScheduleEntry[]> {
|
||||||
|
const persona = await this.ready();
|
||||||
|
const entries = createMonthlyScheduleEntries({ persona, fromDay: datetime, message });
|
||||||
|
await this.emit('persona.schedule.monthly.generated', { count: entries.length, message });
|
||||||
|
await this.memory.saveScheduleEntries(persona.id, entries);
|
||||||
|
await this.refreshAvailability(datetime);
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSchedulesBefore(cutoffExclusive: DateTimeInput): Promise<number> {
|
||||||
|
const persona = await this.ready();
|
||||||
|
const cutoff = toIso(cutoffExclusive);
|
||||||
|
const deleted = await this.memory.deleteScheduleEntriesBefore(persona.id, cutoff);
|
||||||
|
await this.memory.addFact(persona.id, {
|
||||||
|
statement: `Schedules before ${cutoff} were deleted or marked inactive.`,
|
||||||
|
topics: ['persona.schedule.deleted', 'schedule', cutoff.slice(0, 10)],
|
||||||
|
source: 'boxbrain.schedule.prune',
|
||||||
|
metadata: { boxbrainType: 'schedule-deletion', cutoffExclusive: cutoff, deleted },
|
||||||
|
});
|
||||||
|
await this.emit('persona.schedule.deleted', { cutoffExclusive: cutoff, deleted });
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSchedulesOlderThan(datetime: DateTimeInput): Promise<number> {
|
||||||
|
return this.deleteSchedulesBefore(datetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTodayScheduledAvailability(datetime: DateTimeInput): Promise<ScheduledAvailabilitySnapshot> {
|
||||||
|
if (!this.availabilitySnapshot) {
|
||||||
|
await this.refreshAvailability(datetime);
|
||||||
|
}
|
||||||
|
const snapshot = this.availabilitySnapshot;
|
||||||
|
if (!snapshot) throw new Error('Availability snapshot was not initialized.');
|
||||||
|
|
||||||
|
const today = startOfUtcDay(datetime).toISOString();
|
||||||
|
if (snapshot.windowStartAt !== today) {
|
||||||
|
await this.refreshAvailability(datetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshed = this.availabilitySnapshot;
|
||||||
|
if (!refreshed) throw new Error('Availability snapshot was not initialized.');
|
||||||
|
return refreshed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage(input: {
|
||||||
|
datetime: DateTimeInput;
|
||||||
|
messageHistory: PersonaMessage[];
|
||||||
|
getLatestMessageHistory?: () => Promise<PersonaMessage[]>;
|
||||||
|
}): Promise<OutgoingMessageDraft> {
|
||||||
|
const persona = await this.ready();
|
||||||
|
if (!this.options.models?.conversation) {
|
||||||
|
throw new Error('sendMessage requires options.models.conversation.');
|
||||||
|
}
|
||||||
|
const availability = await this.getTodayScheduledAvailability(input.datetime);
|
||||||
|
const context = await buildMandatoryConversationContext({
|
||||||
|
persona,
|
||||||
|
now: input.datetime,
|
||||||
|
memory: this.memory,
|
||||||
|
messages: input.messageHistory,
|
||||||
|
availability,
|
||||||
|
});
|
||||||
|
await this.emit('persona.conversation.context.loaded', {
|
||||||
|
factCount: context.personaAndUserFacts.length,
|
||||||
|
scheduleEntryCount: context.scheduleEntries.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userMessage = [...input.messageHistory].reverse().find((message) => message.sender === 'user')?.content;
|
||||||
|
let draft = ensureDraft(await this.options.models.conversation.generateReply({
|
||||||
|
persona,
|
||||||
|
now: toIso(input.datetime),
|
||||||
|
mode: 'reply',
|
||||||
|
context,
|
||||||
|
...(userMessage === undefined ? {} : { userMessage }),
|
||||||
|
instruction: conversationInstruction(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (input.getLatestMessageHistory && this.options.models.rewrite) {
|
||||||
|
const latest = await input.getLatestMessageHistory();
|
||||||
|
if (latest.length > input.messageHistory.length) {
|
||||||
|
const latestContext = await buildMandatoryConversationContext({
|
||||||
|
persona,
|
||||||
|
now: input.datetime,
|
||||||
|
memory: this.memory,
|
||||||
|
messages: latest,
|
||||||
|
availability,
|
||||||
|
});
|
||||||
|
const decision = await this.options.models.rewrite.decide({
|
||||||
|
persona,
|
||||||
|
now: toIso(input.datetime),
|
||||||
|
previousHistory: input.messageHistory,
|
||||||
|
latestHistory: latest,
|
||||||
|
draft,
|
||||||
|
context: latestContext,
|
||||||
|
});
|
||||||
|
await this.emit('persona.conversation.rewrite.checked', { rewrite: decision.rewrite, reason: decision.reason ?? null });
|
||||||
|
if (decision.rewrite) {
|
||||||
|
draft = ensureDraft(decision.draft ?? await this.options.models.conversation.generateReply({
|
||||||
|
persona,
|
||||||
|
now: toIso(input.datetime),
|
||||||
|
mode: 'reply',
|
||||||
|
context: latestContext,
|
||||||
|
instruction: conversationInstruction(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.emit('persona.conversation.reply.generated', { messageCount: draft.messages.length });
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
async startConversation(input: {
|
||||||
|
datetime: DateTimeInput;
|
||||||
|
messageHistory: PersonaMessage[];
|
||||||
|
}): Promise<OutgoingMessageDraft> {
|
||||||
|
const persona = await this.ready();
|
||||||
|
if (!this.options.models?.conversation) {
|
||||||
|
throw new Error('startConversation requires options.models.conversation.');
|
||||||
|
}
|
||||||
|
const availability = await this.getTodayScheduledAvailability(input.datetime);
|
||||||
|
const context = await buildMandatoryConversationContext({
|
||||||
|
persona,
|
||||||
|
now: input.datetime,
|
||||||
|
memory: this.memory,
|
||||||
|
messages: input.messageHistory,
|
||||||
|
availability,
|
||||||
|
});
|
||||||
|
const draft = ensureDraft(await this.options.models.conversation.generateReply({
|
||||||
|
persona,
|
||||||
|
now: toIso(input.datetime),
|
||||||
|
mode: 'start-conversation',
|
||||||
|
context,
|
||||||
|
instruction: conversationInstruction(),
|
||||||
|
}));
|
||||||
|
await this.emit('persona.conversation.started', { messageCount: draft.messages.length });
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sleepMemory(input: {
|
||||||
|
datetime: DateTimeInput;
|
||||||
|
messageHistory: PersonaMessage[];
|
||||||
|
}): Promise<FactDraft[]> {
|
||||||
|
const persona = await this.ready();
|
||||||
|
if (!this.options.models?.memoryExtraction) {
|
||||||
|
throw new Error('sleepMemory requires options.models.memoryExtraction.');
|
||||||
|
}
|
||||||
|
const contextFacts = await this.memory.findFacts(persona.id, ['persona', persona.displayName, 'user']);
|
||||||
|
const drafts = await this.options.models.memoryExtraction.extract({
|
||||||
|
persona,
|
||||||
|
now: toIso(input.datetime),
|
||||||
|
formattedMessageHistory: formatMessageHistory({ personaName: persona.displayName, messages: input.messageHistory }),
|
||||||
|
contextFacts,
|
||||||
|
instruction: memoryExtractionInstruction(toIso(input.datetime)),
|
||||||
|
});
|
||||||
|
for (const draft of drafts) {
|
||||||
|
await this.memory.addFact(persona.id, {
|
||||||
|
...draft,
|
||||||
|
topics: [...draft.topics, 'sleepMemory'],
|
||||||
|
source: draft.source ?? 'boxbrain.sleepMemory',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.emit('persona.memory.sleep.persisted', { factCount: drafts.length });
|
||||||
|
return drafts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<MemorySpace> {
|
||||||
|
const now = toIso(this.options.now ?? new Date());
|
||||||
|
if (this.mode.type === 'load') {
|
||||||
|
const existing = await this.memory.getSpace(this.mode.spaceId);
|
||||||
|
if (!existing) throw new Error(`Persona space not found: ${this.mode.spaceId}`);
|
||||||
|
await this.emit('persona.loaded', { displayName: existing.displayName });
|
||||||
|
await this.refreshAvailability(now, existing);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const space = await this.memory.createSpace({ displayName: this.mode.displayName, seedMessage: this.mode.seedMessage, now });
|
||||||
|
const modelFacts = this.options.models?.initialization
|
||||||
|
? await this.options.models.initialization.extractInitialFacts({
|
||||||
|
displayName: this.mode.displayName,
|
||||||
|
seedMessage: this.mode.seedMessage,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
const facts = modelFacts ?? [defaultInitialFact(this.mode.displayName, this.mode.seedMessage)];
|
||||||
|
for (const fact of facts) {
|
||||||
|
await this.memory.addFact(space.id, fact);
|
||||||
|
}
|
||||||
|
await this.emit('persona.initialized', { displayName: space.displayName, factCount: facts.length }, space.id);
|
||||||
|
await this.refreshAvailability(now, space);
|
||||||
|
return space;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshAvailability(datetime: DateTimeInput, knownPersona?: MemorySpace): Promise<void> {
|
||||||
|
const persona = knownPersona ?? await this.ready();
|
||||||
|
const start = startOfUtcDay(datetime);
|
||||||
|
const end = addUtcDays(start, 2);
|
||||||
|
const entries = await this.memory.listScheduleEntries(persona.id, start.toISOString(), end.toISOString());
|
||||||
|
this.availabilitySnapshot = buildAvailabilitySnapshot({ now: datetime, entries });
|
||||||
|
await this.emit('persona.availability.refreshed', { rangeCount: this.availabilitySnapshot.ranges.length }, persona.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async emit(name: string, data?: Record<string, unknown>, explicitSpaceId?: string): Promise<void> {
|
||||||
|
if (!this.options.debug) return;
|
||||||
|
const event: DebugEvent = {
|
||||||
|
name,
|
||||||
|
time: new Date().toISOString(),
|
||||||
|
...(explicitSpaceId === undefined ? {} : { spaceId: explicitSpaceId }),
|
||||||
|
...(data === undefined ? {} : { data }),
|
||||||
|
};
|
||||||
|
await this.options.debug(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
211
src/schedule.ts
Normal file
211
src/schedule.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import type {
|
||||||
|
AvailabilityMode,
|
||||||
|
AvailabilityRange,
|
||||||
|
DateTimeInput,
|
||||||
|
MemorySpace,
|
||||||
|
ScheduleActivity,
|
||||||
|
ScheduleEntry,
|
||||||
|
ScheduledAvailabilitySnapshot,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const TEN_MINUTES_MS = 10 * 60 * 1000;
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export function toDate(input: DateTimeInput): Date {
|
||||||
|
const date = input instanceof Date ? new Date(input.getTime()) : new Date(input);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
throw new Error(`Invalid datetime: ${String(input)}`);
|
||||||
|
}
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toIso(input: DateTimeInput): string {
|
||||||
|
return toDate(input).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startOfUtcDay(input: DateTimeInput): Date {
|
||||||
|
const date = toDate(input);
|
||||||
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addUtcDays(input: DateTimeInput, days: number): Date {
|
||||||
|
return new Date(startOfUtcDay(input).getTime() + days * DAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scheduleTargetDay(now: DateTimeInput): Date {
|
||||||
|
return addUtcDays(now, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(activity: ScheduleActivity): { title: string; mode: AvailabilityMode } {
|
||||||
|
switch (activity) {
|
||||||
|
case 'sleep':
|
||||||
|
return { title: 'Sleep', mode: 'offline' };
|
||||||
|
case 'work':
|
||||||
|
return { title: 'Work', mode: 'do-not-disturb' };
|
||||||
|
case 'study':
|
||||||
|
return { title: 'Study', mode: 'do-not-disturb' };
|
||||||
|
case 'job-search':
|
||||||
|
return { title: 'Job search', mode: 'do-not-disturb' };
|
||||||
|
case 'travel':
|
||||||
|
return { title: 'Travel', mode: 'do-not-disturb' };
|
||||||
|
case 'commute':
|
||||||
|
return { title: 'Commute', mode: 'do-not-disturb' };
|
||||||
|
case 'exercise':
|
||||||
|
return { title: 'Exercise', mode: 'online' };
|
||||||
|
case 'meal':
|
||||||
|
return { title: 'Meal', mode: 'online' };
|
||||||
|
case 'social':
|
||||||
|
return { title: 'Social time', mode: 'online' };
|
||||||
|
case 'errand':
|
||||||
|
return { title: 'Errand', mode: 'online' };
|
||||||
|
case 'free-time':
|
||||||
|
return { title: 'Free time', mode: 'online' };
|
||||||
|
case 'rest':
|
||||||
|
return { title: 'Rest', mode: 'online' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseDaytimeActivity(message: string): ScheduleActivity {
|
||||||
|
const lower = message.toLowerCase();
|
||||||
|
if (lower.includes('travel') || lower.includes('trip') || lower.includes('여행')) return 'travel';
|
||||||
|
if (lower.includes('study') || lower.includes('exam') || lower.includes('공부') || lower.includes('시험')) return 'study';
|
||||||
|
if (lower.includes('job') || lower.includes('취업') || lower.includes('구직')) return 'job-search';
|
||||||
|
if (lower.includes('work') || lower.includes('일') || lower.includes('회사')) return 'work';
|
||||||
|
return 'work';
|
||||||
|
}
|
||||||
|
|
||||||
|
function activityForMinute(minuteOfDay: number, message: string): ScheduleActivity {
|
||||||
|
const hour = Math.floor(minuteOfDay / 60);
|
||||||
|
if (hour < 7) return 'sleep';
|
||||||
|
if (hour === 7) return 'meal';
|
||||||
|
if (hour === 8) return 'commute';
|
||||||
|
if (hour >= 9 && hour < 12) return chooseDaytimeActivity(message);
|
||||||
|
if (hour === 12) return 'meal';
|
||||||
|
if (hour >= 13 && hour < 17) return chooseDaytimeActivity(message);
|
||||||
|
if (hour === 17) return 'commute';
|
||||||
|
if (hour === 18) return 'meal';
|
||||||
|
if (hour >= 19 && hour < 21) return message.toLowerCase().includes('study') || message.includes('공부') ? 'study' : 'free-time';
|
||||||
|
if (hour >= 21 && hour < 23) return 'rest';
|
||||||
|
return 'sleep';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTenMinuteDailySchedule(input: {
|
||||||
|
persona: MemorySpace;
|
||||||
|
targetDay: DateTimeInput;
|
||||||
|
message: string;
|
||||||
|
}): ScheduleEntry[] {
|
||||||
|
const target = startOfUtcDay(input.targetDay);
|
||||||
|
const entries: ScheduleEntry[] = [];
|
||||||
|
|
||||||
|
for (let offset = 0; offset < DAY_MS; offset += TEN_MINUTES_MS) {
|
||||||
|
const start = new Date(target.getTime() + offset);
|
||||||
|
const end = new Date(start.getTime() + TEN_MINUTES_MS);
|
||||||
|
const minute = offset / (60 * 1000);
|
||||||
|
const activity = activityForMinute(minute, input.message);
|
||||||
|
const picked = pick(activity);
|
||||||
|
entries.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
spaceId: input.persona.id,
|
||||||
|
startAt: start.toISOString(),
|
||||||
|
endAt: end.toISOString(),
|
||||||
|
activity,
|
||||||
|
title: picked.title,
|
||||||
|
description: `Realistic ${picked.title.toLowerCase()} block for ${input.persona.displayName}.`,
|
||||||
|
granularity: 'ten-minute',
|
||||||
|
sourceMessage: input.message,
|
||||||
|
metadata: {
|
||||||
|
boxbrainType: 'schedule-entry',
|
||||||
|
availabilityMode: picked.mode,
|
||||||
|
targetDate: target.toISOString().slice(0, 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMonthlyScheduleEntries(input: {
|
||||||
|
persona: MemorySpace;
|
||||||
|
fromDay: DateTimeInput;
|
||||||
|
message: string;
|
||||||
|
days?: number;
|
||||||
|
}): ScheduleEntry[] {
|
||||||
|
const start = scheduleTargetDay(input.fromDay);
|
||||||
|
const count = input.days ?? 30;
|
||||||
|
const entries: ScheduleEntry[] = [];
|
||||||
|
for (let day = 0; day < count; day += 1) {
|
||||||
|
const dayStart = new Date(start.getTime() + day * DAY_MS);
|
||||||
|
const travelHint = day > 0 && day % 90 === 0 ? ' travel' : '';
|
||||||
|
const activity = chooseDaytimeActivity(`${input.message}${travelHint}`);
|
||||||
|
const picked = pick(activity);
|
||||||
|
entries.push({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
spaceId: input.persona.id,
|
||||||
|
startAt: dayStart.toISOString(),
|
||||||
|
endAt: new Date(dayStart.getTime() + DAY_MS).toISOString(),
|
||||||
|
activity,
|
||||||
|
title: picked.title,
|
||||||
|
description: `Daily outline for ${input.persona.displayName}.`,
|
||||||
|
granularity: 'day',
|
||||||
|
sourceMessage: input.message,
|
||||||
|
metadata: {
|
||||||
|
boxbrainType: 'schedule-entry',
|
||||||
|
availabilityMode: picked.mode,
|
||||||
|
targetDate: dayStart.toISOString().slice(0, 10),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function availabilityModeForEntry(entry: ScheduleEntry): AvailabilityMode {
|
||||||
|
const mode = entry.metadata['availabilityMode'];
|
||||||
|
if (mode === 'online' || mode === 'do-not-disturb' || mode === 'offline') return mode;
|
||||||
|
if (entry.activity === 'sleep') return 'offline';
|
||||||
|
if (entry.activity === 'work' || entry.activity === 'study' || entry.activity === 'job-search' || entry.activity === 'travel' || entry.activity === 'commute') {
|
||||||
|
return 'do-not-disturb';
|
||||||
|
}
|
||||||
|
return 'online';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAvailabilitySnapshot(input: {
|
||||||
|
now: DateTimeInput;
|
||||||
|
generatedAt?: DateTimeInput;
|
||||||
|
entries: ScheduleEntry[];
|
||||||
|
}): ScheduledAvailabilitySnapshot {
|
||||||
|
const windowStart = startOfUtcDay(input.now);
|
||||||
|
const windowEnd = new Date(windowStart.getTime() + 2 * DAY_MS);
|
||||||
|
const sorted = input.entries
|
||||||
|
.filter((entry) => entry.startAt < windowEnd.toISOString() && entry.endAt > windowStart.toISOString())
|
||||||
|
.sort((a, b) => a.startAt.localeCompare(b.startAt));
|
||||||
|
|
||||||
|
const ranges: AvailabilityRange[] = [];
|
||||||
|
for (const entry of sorted) {
|
||||||
|
const mode = availabilityModeForEntry(entry);
|
||||||
|
const previous = ranges.at(-1);
|
||||||
|
if (previous && previous.mode === mode && previous.endAt === entry.startAt) {
|
||||||
|
previous.endAt = entry.endAt;
|
||||||
|
previous.sourceScheduleIds.push(entry.id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ranges.push({
|
||||||
|
startAt: entry.startAt,
|
||||||
|
endAt: entry.endAt,
|
||||||
|
mode,
|
||||||
|
sourceScheduleIds: [entry.id],
|
||||||
|
reason: entry.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt: toIso(input.generatedAt ?? input.now),
|
||||||
|
windowStartAt: windowStart.toISOString(),
|
||||||
|
windowEndAt: windowEnd.toISOString(),
|
||||||
|
ranges,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateKeysAround(input: DateTimeInput): string[] {
|
||||||
|
const today = startOfUtcDay(input);
|
||||||
|
return [-1, 0, 1].map((offset) => new Date(today.getTime() + offset * DAY_MS).toISOString().slice(0, 10));
|
||||||
|
}
|
||||||
174
src/types.ts
Normal file
174
src/types.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
export type DateTimeInput = Date | string | number;
|
||||||
|
|
||||||
|
export type PersonaConstructorMode = 'create' | 'load';
|
||||||
|
|
||||||
|
export type ScheduleGranularity = 'day' | 'ten-minute';
|
||||||
|
|
||||||
|
export type ScheduleActivity =
|
||||||
|
| 'sleep'
|
||||||
|
| 'rest'
|
||||||
|
| 'meal'
|
||||||
|
| 'commute'
|
||||||
|
| 'work'
|
||||||
|
| 'study'
|
||||||
|
| 'job-search'
|
||||||
|
| 'travel'
|
||||||
|
| 'exercise'
|
||||||
|
| 'social'
|
||||||
|
| 'errand'
|
||||||
|
| 'free-time';
|
||||||
|
|
||||||
|
export type AvailabilityMode = 'online' | 'do-not-disturb' | 'offline';
|
||||||
|
|
||||||
|
export interface MemorySpace {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
createdAt: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactDraft {
|
||||||
|
statement: string;
|
||||||
|
topics: string[];
|
||||||
|
confidence?: number;
|
||||||
|
source?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoredFact extends FactDraft {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleEntry {
|
||||||
|
id: string;
|
||||||
|
spaceId: string;
|
||||||
|
startAt: string;
|
||||||
|
endAt: string;
|
||||||
|
activity: ScheduleActivity;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
granularity: ScheduleGranularity;
|
||||||
|
sourceMessage?: string;
|
||||||
|
metadata: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailabilityRange {
|
||||||
|
startAt: string;
|
||||||
|
endAt: string;
|
||||||
|
mode: AvailabilityMode;
|
||||||
|
sourceScheduleIds: string[];
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduledAvailabilitySnapshot {
|
||||||
|
generatedAt: string;
|
||||||
|
windowStartAt: string;
|
||||||
|
windowEndAt: string;
|
||||||
|
ranges: AvailabilityRange[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonaMessage {
|
||||||
|
sender: 'persona' | 'user';
|
||||||
|
time: DateTimeInput;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DebugEvent {
|
||||||
|
name: string;
|
||||||
|
time: string;
|
||||||
|
spaceId?: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DebugHook = (event: DebugEvent) => void | Promise<void>;
|
||||||
|
|
||||||
|
export interface MandatoryConversationContext {
|
||||||
|
formattedMessageHistory: string;
|
||||||
|
conversationWindowLabel: string;
|
||||||
|
memorySummary: string;
|
||||||
|
personaAndUserFacts: StoredFact[];
|
||||||
|
scheduleEntries: ScheduleEntry[];
|
||||||
|
availability: ScheduledAvailabilitySnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReplyGenerationInput {
|
||||||
|
persona: MemorySpace;
|
||||||
|
now: string;
|
||||||
|
mode: 'reply' | 'start-conversation';
|
||||||
|
context: MandatoryConversationContext;
|
||||||
|
userMessage?: string;
|
||||||
|
instruction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutgoingMessageDraft {
|
||||||
|
messages: string[];
|
||||||
|
reasoning?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewriteDecisionInput {
|
||||||
|
persona: MemorySpace;
|
||||||
|
now: string;
|
||||||
|
previousHistory: PersonaMessage[];
|
||||||
|
latestHistory: PersonaMessage[];
|
||||||
|
draft: OutgoingMessageDraft;
|
||||||
|
context: MandatoryConversationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewriteDecision {
|
||||||
|
rewrite: boolean;
|
||||||
|
draft?: OutgoingMessageDraft;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConversationModel {
|
||||||
|
generateReply(input: ReplyGenerationInput): Promise<OutgoingMessageDraft>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RewriteModel {
|
||||||
|
decide(input: RewriteDecisionInput): Promise<RewriteDecision>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryExtractionInput {
|
||||||
|
persona: MemorySpace;
|
||||||
|
now: string;
|
||||||
|
formattedMessageHistory: string;
|
||||||
|
contextFacts: StoredFact[];
|
||||||
|
instruction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MemoryExtractionModel {
|
||||||
|
extract(input: MemoryExtractionInput): Promise<FactDraft[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonaInitializationModel {
|
||||||
|
extractInitialFacts(input: {
|
||||||
|
displayName: string;
|
||||||
|
seedMessage: string;
|
||||||
|
now: string;
|
||||||
|
}): Promise<FactDraft[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonaModels {
|
||||||
|
initialization?: PersonaInitializationModel;
|
||||||
|
conversation?: ConversationModel;
|
||||||
|
rewrite?: RewriteModel;
|
||||||
|
memoryExtraction?: MemoryExtractionModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersonaOptions {
|
||||||
|
memory?: BoxBrainMemoryStore;
|
||||||
|
models?: PersonaModels;
|
||||||
|
debug?: DebugHook;
|
||||||
|
now?: DateTimeInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BoxBrainMemoryStore {
|
||||||
|
createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise<MemorySpace>;
|
||||||
|
getSpace(spaceId: string): Promise<MemorySpace | null>;
|
||||||
|
addFact(spaceId: string, fact: FactDraft): Promise<StoredFact>;
|
||||||
|
findFacts(spaceId: string, topics: string[]): Promise<StoredFact[]>;
|
||||||
|
saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise<void>;
|
||||||
|
listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise<ScheduleEntry[]>;
|
||||||
|
deleteScheduleEntriesBefore(spaceId: string, cutoffExclusive: string): Promise<number>;
|
||||||
|
}
|
||||||
117
tests/conversation.test.ts
Normal file
117
tests/conversation.test.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { InMemoryMemoryStore, Persona, type ReplyGenerationInput } from '../src';
|
||||||
|
|
||||||
|
describe('Conversation API', () => {
|
||||||
|
it('loads mandatory memory, schedules, availability, and formatted history before replying', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
let captured: ReplyGenerationInput | undefined;
|
||||||
|
const persona = new Persona('Mina', 'Mina likes quiet cafes.', {
|
||||||
|
memory,
|
||||||
|
now: '2026-05-01T10:00:00.000Z',
|
||||||
|
models: {
|
||||||
|
conversation: {
|
||||||
|
async generateReply(input) {
|
||||||
|
captured = input;
|
||||||
|
return { messages: ['카페에 있었어.', '너는 뭐해?'] };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const space = await persona.ready();
|
||||||
|
await memory.addFact(space.id, { statement: 'The user is close to Mina.', topics: ['user', 'persona'] });
|
||||||
|
await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'study for an exam');
|
||||||
|
|
||||||
|
const reply = await persona.sendMessage({
|
||||||
|
datetime: '2026-05-01T12:00:00.000Z',
|
||||||
|
messageHistory: [
|
||||||
|
{ sender: 'persona', time: '2026-04-30T23:00:00.000Z', content: '다음에 보자' },
|
||||||
|
{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '지금 뭐해?' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reply.messages).toEqual(['카페에 있었어.', '너는 뭐해?']);
|
||||||
|
expect(captured?.context.formattedMessageHistory).toContain('Mina@2026-04-30T23:00:00.000Z: 다음에 보자');
|
||||||
|
expect(captured?.context.formattedMessageHistory).toContain('user@2026-05-01T12:00:00.000Z: 지금 뭐해?');
|
||||||
|
expect(captured?.context.memorySummary).toContain('The user is close to Mina.');
|
||||||
|
expect(captured?.context.scheduleEntries.length).toBeGreaterThan(0);
|
||||||
|
expect(captured?.context.availability.ranges.length).toBeGreaterThan(0);
|
||||||
|
expect(captured?.instruction).toContain('send_message');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('explicitly tells the response model when mandatory memory is missing', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
let memorySummary = '';
|
||||||
|
const persona = new Persona('Mina', 'Mina is new.', {
|
||||||
|
memory,
|
||||||
|
now: '2026-05-01T10:00:00.000Z',
|
||||||
|
models: {
|
||||||
|
initialization: { async extractInitialFacts() { return []; } },
|
||||||
|
conversation: {
|
||||||
|
async generateReply(input) {
|
||||||
|
memorySummary = input.context.memorySummary;
|
||||||
|
return { messages: ['잘 모르겠어.'] };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await persona.ready();
|
||||||
|
|
||||||
|
await persona.sendMessage({
|
||||||
|
datetime: '2026-05-01T12:00:00.000Z',
|
||||||
|
messageHistory: [{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '나 기억해?' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(memorySummary).toBe('기억이 없음');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets a rewrite model discard a stale draft when new user messages arrive', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const persona = new Persona('Mina', 'Mina is concise.', {
|
||||||
|
memory,
|
||||||
|
now: '2026-05-01T10:00:00.000Z',
|
||||||
|
models: {
|
||||||
|
conversation: { async generateReply() { return { messages: ['첫 답장'] }; } },
|
||||||
|
rewrite: {
|
||||||
|
async decide() {
|
||||||
|
return { rewrite: true, draft: { messages: ['새 메시지까지 보고 답장'] }, reason: 'latest user message changes intent' };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await persona.ready();
|
||||||
|
|
||||||
|
const reply = await persona.sendMessage({
|
||||||
|
datetime: '2026-05-01T12:00:00.000Z',
|
||||||
|
messageHistory: [{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '안녕' }],
|
||||||
|
getLatestMessageHistory: async () => [
|
||||||
|
{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: '안녕' },
|
||||||
|
{ sender: 'user', time: '2026-05-01T12:00:05.000Z', content: '아 맞다, 지금 바빠?' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reply.messages).toEqual(['새 메시지까지 보고 답장']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can proactively start a conversation with the same mandatory context pipeline', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
let mode = '';
|
||||||
|
const persona = new Persona('Mina', 'Mina starts soft conversations.', {
|
||||||
|
memory,
|
||||||
|
now: '2026-05-01T10:00:00.000Z',
|
||||||
|
models: {
|
||||||
|
conversation: {
|
||||||
|
async generateReply(input) {
|
||||||
|
mode = input.mode;
|
||||||
|
return { messages: ['오늘 좀 조용하네.'] };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await persona.ready();
|
||||||
|
|
||||||
|
const started = await persona.startConversation({ datetime: '2026-05-01T20:00:00.000Z', messageHistory: [] });
|
||||||
|
|
||||||
|
expect(mode).toBe('start-conversation');
|
||||||
|
expect(started.messages).toEqual(['오늘 좀 조용하네.']);
|
||||||
|
});
|
||||||
|
});
|
||||||
43
tests/persona.test.ts
Normal file
43
tests/persona.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { InMemoryMemoryStore, Persona, type FactDraft } from '../src';
|
||||||
|
|
||||||
|
describe('Persona initialization', () => {
|
||||||
|
it('creates a new isolated persona space from displayName and seed message', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const debug: string[] = [];
|
||||||
|
const persona = new Persona('Mina', 'Mina is a careful student who likes quiet cafes.', {
|
||||||
|
memory,
|
||||||
|
now: '2026-05-01T10:00:00.000Z',
|
||||||
|
debug: (event) => { debug.push(event.name); },
|
||||||
|
models: {
|
||||||
|
initialization: {
|
||||||
|
async extractInitialFacts(input): Promise<FactDraft[]> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
statement: `${input.displayName} likes quiet cafes.`,
|
||||||
|
topics: ['persona', input.displayName],
|
||||||
|
source: 'test',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const space = await persona.ready();
|
||||||
|
expect(space.displayName).toBe('Mina');
|
||||||
|
expect(space.id).toMatch(/^persona-mina-/);
|
||||||
|
expect(await memory.findFacts(space.id, ['persona'])).toHaveLength(1);
|
||||||
|
expect(debug).toContain('persona.initialized');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads an existing persona space by space id without creating another space', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const created = new Persona('Joon', 'Joon is a freelance designer.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||||
|
const space = await created.ready();
|
||||||
|
|
||||||
|
const loaded = new Persona(space.id, { memory, now: '2026-05-01T11:00:00.000Z' });
|
||||||
|
await expect(loaded.ready()).resolves.toMatchObject({ id: space.id, displayName: 'Joon' });
|
||||||
|
expect(memory.spaces.size).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
50
tests/schedule.test.ts
Normal file
50
tests/schedule.test.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { InMemoryMemoryStore, Persona } from '../src';
|
||||||
|
|
||||||
|
describe('Persona schedules and availability', () => {
|
||||||
|
it('creates tomorrow as a ten-minute daily schedule and persists it in memory', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const persona = new Persona('Mina', 'Mina works weekdays and studies at night.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||||
|
const space = await persona.ready();
|
||||||
|
|
||||||
|
const entries = await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.');
|
||||||
|
|
||||||
|
expect(entries).toHaveLength(144);
|
||||||
|
expect(entries[0]).toMatchObject({
|
||||||
|
spaceId: space.id,
|
||||||
|
startAt: '2026-05-02T00:00:00.000Z',
|
||||||
|
endAt: '2026-05-02T00:10:00.000Z',
|
||||||
|
granularity: 'ten-minute',
|
||||||
|
});
|
||||||
|
expect(entries.at(-1)?.endAt).toBe('2026-05-03T00:00:00.000Z');
|
||||||
|
await expect(memory.listScheduleEntries(space.id, '2026-05-02T00:00:00.000Z', '2026-05-03T00:00:00.000Z')).resolves.toHaveLength(144);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives online, do-not-disturb, and offline availability from the in-memory schedule window', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const persona = new Persona('Mina', 'Mina works weekdays and studies at night.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||||
|
await persona.ready();
|
||||||
|
|
||||||
|
await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.');
|
||||||
|
const availability = await persona.getTodayScheduledAvailability('2026-05-01T12:00:00.000Z');
|
||||||
|
|
||||||
|
expect(availability.windowStartAt).toBe('2026-05-01T00:00:00.000Z');
|
||||||
|
expect(availability.windowEndAt).toBe('2026-05-03T00:00:00.000Z');
|
||||||
|
expect(new Set(availability.ranges.map((range) => range.mode))).toEqual(new Set(['offline', 'online', 'do-not-disturb']));
|
||||||
|
expect(availability.ranges.find((range) => range.mode === 'offline')?.startAt).toBe('2026-05-02T00:00:00.000Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prunes schedule entries before a caller-provided cutoff', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const persona = new Persona('Mina', 'Mina works weekdays.', { memory, now: '2026-05-01T10:00:00.000Z' });
|
||||||
|
const space = await persona.ready();
|
||||||
|
await persona.createDailySchedule('2026-05-01T10:00:00.000Z', 'Keep a normal work day.');
|
||||||
|
|
||||||
|
const deleted = await persona.deleteSchedulesBefore('2026-05-02T12:00:00.000Z');
|
||||||
|
|
||||||
|
expect(deleted).toBe(72);
|
||||||
|
await expect(memory.listScheduleEntries(space.id, '2026-05-02T00:00:00.000Z', '2026-05-03T00:00:00.000Z')).resolves.toHaveLength(72);
|
||||||
|
const deletionFacts = await memory.findFacts(space.id, ['persona.schedule.deleted']);
|
||||||
|
expect(deletionFacts[0]?.metadata?.['deleted']).toBe(72);
|
||||||
|
});
|
||||||
|
});
|
||||||
38
tests/sleep-memory.test.ts
Normal file
38
tests/sleep-memory.test.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { InMemoryMemoryStore, Persona } from '../src';
|
||||||
|
|
||||||
|
describe('sleepMemory', () => {
|
||||||
|
it('objectivizes and persists extracted durable facts through the memory model', async () => {
|
||||||
|
const memory = new InMemoryMemoryStore();
|
||||||
|
const persona = new Persona('Mina', 'Mina remembers stable details.', {
|
||||||
|
memory,
|
||||||
|
now: '2026-05-01T10:00:00.000Z',
|
||||||
|
models: {
|
||||||
|
memoryExtraction: {
|
||||||
|
async extract(input) {
|
||||||
|
expect(input.formattedMessageHistory).toContain('user@2026-05-01T15:00:00.000Z: 나는 타입스크립트를 2025년부터 시작했어');
|
||||||
|
expect(input.instruction).toContain('Objectivize');
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
statement: 'The user started TypeScript in 2025.',
|
||||||
|
topics: ['user', 'TypeScript', '2025'],
|
||||||
|
confidence: 0.9,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const space = await persona.ready();
|
||||||
|
|
||||||
|
const drafts = await persona.sleepMemory({
|
||||||
|
datetime: '2026-05-02T00:00:00.000Z',
|
||||||
|
messageHistory: [{ sender: 'user', time: '2026-05-01T15:00:00.000Z', content: '나는 타입스크립트를 2025년부터 시작했어' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(drafts).toHaveLength(1);
|
||||||
|
const facts = await memory.findFacts(space.id, ['TypeScript']);
|
||||||
|
expect(facts[0]).toMatchObject({ statement: 'The user started TypeScript in 2025.', source: 'boxbrain.sleepMemory' });
|
||||||
|
expect(facts[0]?.topics).toContain('sleepMemory');
|
||||||
|
});
|
||||||
|
});
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"types": ["bun-types"],
|
||||||
|
"strict": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
|
},
|
||||||
|
"include": ["src", "tests"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user