@ykurilov/agent-chat-memory 0.0.0-stage → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +648 -0
- package/LICENSE +21 -0
- package/README.md +136 -2
- package/dist/chunk-VOET63ER.js +55 -0
- package/dist/chunk-VOET63ER.js.map +1 -0
- package/dist/index.js +647 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.js +15 -0
- package/dist/labels.js.map +1 -0
- package/dist/types/create-long-term-memory.d.ts +37 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/instructions.d.ts +2 -0
- package/dist/types/manage-memory-tool.d.ts +4 -0
- package/dist/types/memory-block.d.ts +14 -0
- package/dist/types/memory-operations.d.ts +41 -0
- package/dist/types/note-rules.d.ts +33 -0
- package/dist/types/optimizer/merge-history.d.ts +27 -0
- package/dist/types/optimizer/merge-plan.d.ts +43 -0
- package/dist/types/optimizer/propose-merges.d.ts +26 -0
- package/dist/types/optimizer/protected-details.d.ts +11 -0
- package/dist/types/optimizer/run-optimization.d.ts +45 -0
- package/dist/types/owner-lock.d.ts +4 -0
- package/dist/types/search-notes.d.ts +7 -0
- package/dist/types/tool-titles.d.ts +24 -0
- package/package.json +45 -3
package/README.md
CHANGED
|
@@ -1,3 +1,137 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ykurilov/agent-chat-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Долгая память агента: заметки о человеке и его правила («отвечай на ты», «не предлагай npm») переживают чаты.
|
|
4
|
+
Агент сам решает, что запомнить, по ходу разговора; человек видит записи на экране памяти и правит их.
|
|
5
|
+
Отдельное расширение: приложению, которому память не нужна, она не нужна и в сборке.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @ykurilov/agent-chat-memory
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Подключение
|
|
12
|
+
|
|
13
|
+
Сервер: память собирается из хранилища чатов и передаётся обработчику. Адреса `{base}/memory` для экрана появляются сами.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createLongTermMemory } from "@ykurilov/agent-chat-memory";
|
|
17
|
+
|
|
18
|
+
const memory = createLongTermMemory({ storage }); // SQLite, PostgreSQL или хранилище в памяти
|
|
19
|
+
|
|
20
|
+
createChatHandler({
|
|
21
|
+
// …остальное как в docs/getting-started.md, шаг 4
|
|
22
|
+
storage,
|
|
23
|
+
memory,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Ядро (`createChatService({ …, memory })`) принимает то же самое; обработчик только передаёт опцию дальше.
|
|
28
|
+
|
|
29
|
+
Браузер: экран памяти и названия строк вызова.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { AgentMemory, ChatLabelsProvider, createMemoryApi, russianLabels } from "@ykurilov/agent-chat-react";
|
|
33
|
+
import { tandemMemorySkin } from "@ykurilov/agent-chat-skin-tandem";
|
|
34
|
+
import { withMemoryToolTitles } from "@ykurilov/agent-chat-memory/labels";
|
|
35
|
+
import { useMemo } from "react";
|
|
36
|
+
|
|
37
|
+
function MemoryPage() {
|
|
38
|
+
const memoryApi = useMemo(() => createMemoryApi({ api: "/api/chat" }), []); // один раз: при смене объекта список грузится заново
|
|
39
|
+
return <AgentMemory api={memoryApi} skin={tandemMemorySkin} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// В ленте чата вызов инструмента называется «Работа с памятью», а не `manage_memory`:
|
|
43
|
+
<ChatLabelsProvider labels={withMemoryToolTitles(russianLabels)}>…</ChatLabelsProvider>;
|
|
44
|
+
// по-английски: withMemoryToolTitles(englishLabels, englishMemoryToolTitles)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Оба словаря есть и в корне пакета: `import { englishMemoryToolTitles, memoryToolTitles } from "@ykurilov/agent-chat-memory"`.
|
|
48
|
+
|
|
49
|
+
`./labels` — отдельная точка входа без кода сервера: безопасна в браузерной сборке. Подпись можно вложить в другие:
|
|
50
|
+
`withMemoryToolTitles(withHistorySearchTitles(russianLabels))`.
|
|
51
|
+
|
|
52
|
+
## Что делает подключение
|
|
53
|
+
|
|
54
|
+
- К инструкции агента добавляется постоянный текст о памяти (один раз), к инструментам — `manage_memory`. Имя инструмента занято: одноимённый инструмент приложения — ошибка при создании.
|
|
55
|
+
- Записи человека попадают в вопрос первого хода чата: шапка `<agent_memory>` и по блоку на запись. Как файл правил в Claude Code, память читается один раз на чат: в идущем чате она не перечитывается, и новые, изменённые и удалённые записи (в том числе записанные самим агентом — их он видит в переписке) на каждом ходе не досылаются. Память читается заново: в новом чате, после сжатия истории, при правке первого сообщения или «Переделать» первого ответа, в первом личном ходе после группового и в ходе после сбоя чтения. Если после сжатия в хвосте остался прошлый ответ, в новый вопрос идут новые и изменённые записи и строка `[Больше не действует: «memory:<id>»]` про удалённые. Запись, которая не влезла в `maxBlockChars`, — не удалённая: `blocks()` отдаёт её как `{ key: "memory:<id>", hidden: true }`, и модель, видевшая её раньше, получает строку `[Не показано ради места, но по-прежнему действует: «memory:<id>»]`.
|
|
56
|
+
- Поэтому начало запроса от хода к ходу то же, и кэш модели сохраняется: правка одной записи стоит одной строки, а не всего блока. В историю на экране, в сжатие и в поиск по переписке блок не попадает; он хранится служебной частью ответа. Проверяет тест в ядре.
|
|
57
|
+
- Сбой чтения памяти не ломает ответ: ход идёт без блока, ошибка в журнале сервера. Следующий ход того же чата попробует прочитать память снова.
|
|
58
|
+
- Хранилище без методов памяти отклоняется при создании (`MemoryStorageNotSupportedError` со списком недостающих методов).
|
|
59
|
+
|
|
60
|
+
## Инструмент `manage_memory`
|
|
61
|
+
|
|
62
|
+
| Действие | Вход | Ответ |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `create` | `notes: [{ categories, content }]`, от 1 до 5 записей | `{ notes }` |
|
|
65
|
+
| `update` | `id`, `version`, `categories`, `content` | `{ note }` |
|
|
66
|
+
| `delete` | `id`, `version` | `{ deleted }` |
|
|
67
|
+
| `search` | `query` | `{ notes, total }`, до 10 записей |
|
|
68
|
+
|
|
69
|
+
Ошибка приходит ответом `{ error: { kind, message } }`, а не исключением: `invalid`, `duplicate`, `limit_reached`, `not_found`, `stale_version`.
|
|
70
|
+
В `message` подсказка, что делать дальше («найди запись заново и повтори»). `id` и `version` агент берёт из блока памяти или из поиска.
|
|
71
|
+
|
|
72
|
+
Запись: от 1 до 3 разделов (до 60 знаков, без «/») и текст до 500 знаков. Дубль (тот же текст в том же разделе, без учёта регистра и лишних пробелов) отклоняется.
|
|
73
|
+
|
|
74
|
+
## Пределы
|
|
75
|
+
|
|
76
|
+
`maxNotes` 200 записей на человека, `maxTotalChars` 48 000 знаков на все записи, `maxBatch` 5 записей в одном `create`, `maxBlockChars` 12 000 знаков в блоке запроса.
|
|
77
|
+
Не поместилось в блок — в нём самые недавно изменённые, остальные агент находит поиском (строка «Ещё N записей не показаны»), а экран памяти пишет, сколько показано агенту.
|
|
78
|
+
|
|
79
|
+
## Адреса сервера
|
|
80
|
+
|
|
81
|
+
Человек определяется `resolveUser`. Без опции `memory` все адреса отвечают `404`.
|
|
82
|
+
|
|
83
|
+
| Запрос | Ответ |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `GET {base}/memory` | `{ notes, counts: { total, limit, shown } }` |
|
|
86
|
+
| `POST {base}/memory` с `{ categories, content }` | `201 { note }` |
|
|
87
|
+
| `PATCH {base}/memory/:id` с `{ categories, content, version }` | `{ note }` |
|
|
88
|
+
| `DELETE {base}/memory/:id?version=N` | `204` |
|
|
89
|
+
| `DELETE {base}/memory` (стереть всю память) | `{ deleted: N }` |
|
|
90
|
+
|
|
91
|
+
| `GET {base}/memory/merges` | `{ merges }`: склейки, которые ещё можно вернуть |
|
|
92
|
+
| `POST {base}/memory/merges/:runId/revert` | `{ reverted: runId }` |
|
|
93
|
+
|
|
94
|
+
Ошибки: `not_found` 404 (чужая запись — тоже 404), `stale_version`, `limit_reached` и `duplicate` — 409, `invalid` — 400.
|
|
95
|
+
Адреса склеек отвечают `404`, если хранилище склейку не умеет. Возврат склейки отвечает `409` с `{ error: { kind } }`: `conflict` (записи после склейки менялись),
|
|
96
|
+
`limit_reached` (после возврата записей стало бы больше предела) или `duplicate` (одну из записей уже добавили заново). Экран пишет, что делать.
|
|
97
|
+
|
|
98
|
+
## Склейка повторов
|
|
99
|
+
|
|
100
|
+
После ответа агента память раз в сутки просит ту же модель, что вела ход, найти в записях человека повторы. Модель предлагает группы, код каждую проверяет
|
|
101
|
+
(состав, пределы, не потерялись ли цифры, даты, имена и отрицание: «не любит кофе» и «любит кофе» не склеиваются) и применяет только годные.
|
|
102
|
+
Экран памяти показывает раздел «Объединено» с исходными записями и кнопкой «Вернуть как было». Отдельной модели для проверки нет.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
createLongTermMemory({ storage }); // включена, если хранилище умеет
|
|
106
|
+
createLongTermMemory({ storage, optimizer: { intervalHours: 12 } }); // раз в 12 часов; хранилище обязано уметь
|
|
107
|
+
createLongTermMemory({ storage, optimizer: false }); // выключена
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Пауза после сбоев.** Если запуск не удался (модель не дала разбираемый ответ, обрыв, отказ проверки), следующий ждёт час, после второго подряд сбоя — 6 часов,
|
|
111
|
+
после третьего и дальше — сутки. Удачный запуск (даже «повторов нет») сбрасывает счёт. Запуск, который завис и потерял право на работу, тоже считается сбоем.
|
|
112
|
+
|
|
113
|
+
## Одновременные действия
|
|
114
|
+
|
|
115
|
+
Пределы и дубли проверяются под замком на человека: два вызова подряд (агент и экран, две вкладки) не проскочат мимо `maxNotes`, `maxTotalChars` и проверки дублей.
|
|
116
|
+
Возврат склейки проверяет пределы и дубли так же, как обычное добавление.
|
|
117
|
+
|
|
118
|
+
## Свой скин экрана памяти
|
|
119
|
+
|
|
120
|
+
Договор скина версии 11: отдельный `MemorySkin` (`MemoryScreen`, `MemoryGroup`, `MemoryNoteRow`, `MemoryNoteEditor`, `MemoryConfirm`),
|
|
121
|
+
`ChatSkin` не менялся. Скин рисует готовые данные и зовёт колбэки; все надписи берёт из `useChatLabels()`. Подробно — `specs/017-long-term-memory/contracts/memory.md`, раздел 5.
|
|
122
|
+
|
|
123
|
+
## Хранилища
|
|
124
|
+
|
|
125
|
+
SQLite и PostgreSQL получают таблицы сами при открытии базы: записи памяти — SQLite `0009`, PostgreSQL `0008`; склейка повторов — SQLite `0010`, PostgreSQL `0009`.
|
|
126
|
+
Хранилище в памяти тоже умеет. Своему хранилищу нужны пять необязательных методов `ChatStorage` и общие тесты `describeChatStorageContract`:
|
|
127
|
+
`listMemoryNotes`, `insertMemoryNotes`, `updateMemoryNote`, `deleteMemoryNote`, `clearMemoryNotes`. Удаление чата память не трогает.
|
|
128
|
+
|
|
129
|
+
Для склейки нужны ещё шесть: `claimMemoryOptimizationRun`, `finishMemoryOptimizationRun`, `applyMemoryMerge`, `revertMemoryMerge`, `listMemoryOptimizationRuns`, `listMergedMemoryNotes`.
|
|
130
|
+
Без них склейка тихо выключена (или ошибка, если задана опция `optimizer`).
|
|
131
|
+
|
|
132
|
+
Чтобы пределы держались и при нескольких серверах, у хранилища есть два необязательных метода: `insertMemoryNotesChecked` и `updateMemoryNoteChecked`.
|
|
133
|
+
Они зовут проверку внутри своей транзакции (SQLite — транзакция, PostgreSQL — `pg_advisory_xact_lock` на человека). У `revertMemoryMerge` тоже есть необязательная проверка.
|
|
134
|
+
Своё хранилище без этих методов работает как раньше, а пределы держит замок внутри одного процесса; при нескольких серверах общий предел тогда не гарантирован.
|
|
135
|
+
Правила — `specs/017-long-term-memory/contracts/memory.md`.
|
|
136
|
+
|
|
137
|
+
Спецификация: `specs/017-long-term-memory`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/tool-titles.ts
|
|
2
|
+
var memoryToolTitles = {
|
|
3
|
+
manage_memory: "\u0420\u0430\u0431\u043E\u0442\u0430 \u0441 \u043F\u0430\u043C\u044F\u0442\u044C\u044E"
|
|
4
|
+
};
|
|
5
|
+
var englishMemoryToolTitles = {
|
|
6
|
+
manage_memory: "Working with memory"
|
|
7
|
+
};
|
|
8
|
+
function memoryPhrase(byAction, general) {
|
|
9
|
+
return ({ input }) => {
|
|
10
|
+
const action = typeof input === "object" && input !== null ? input.action : void 0;
|
|
11
|
+
return typeof action === "string" && Object.hasOwn(byAction, action) ? byAction[action] : general;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var memoryStepPhrases = {
|
|
15
|
+
manage_memory: memoryPhrase(
|
|
16
|
+
{
|
|
17
|
+
create: { running: "\u0417\u0430\u043F\u043E\u043C\u0438\u043D\u0430\u044E", done: "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B" },
|
|
18
|
+
update: { running: "\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u044E \u043F\u0430\u043C\u044F\u0442\u044C", done: "\u041E\u0431\u043D\u043E\u0432\u0438\u043B \u043F\u0430\u043C\u044F\u0442\u044C" },
|
|
19
|
+
delete: { running: "\u0417\u0430\u0431\u044B\u0432\u0430\u044E \u043B\u0438\u0448\u043D\u0435\u0435", done: "\u0417\u0430\u0431\u044B\u043B \u043B\u0438\u0448\u043D\u0435\u0435" },
|
|
20
|
+
search: { running: "\u0412\u0441\u043F\u043E\u043C\u0438\u043D\u0430\u044E", done: "\u0412\u0441\u043F\u043E\u043C\u043D\u0438\u043B" }
|
|
21
|
+
},
|
|
22
|
+
{ running: "\u0420\u0430\u0431\u043E\u0442\u0430\u044E \u0441 \u043F\u0430\u043C\u044F\u0442\u044C\u044E", done: "\u041F\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u043B \u0441 \u043F\u0430\u043C\u044F\u0442\u044C\u044E" }
|
|
23
|
+
)
|
|
24
|
+
};
|
|
25
|
+
var englishMemoryStepPhrases = {
|
|
26
|
+
manage_memory: memoryPhrase(
|
|
27
|
+
{
|
|
28
|
+
create: { running: "Remembering", done: "Remembered" },
|
|
29
|
+
update: { running: "Updating memory", done: "Updated memory" },
|
|
30
|
+
delete: { running: "Forgetting what\u2019s outdated", done: "Forgot what\u2019s outdated" },
|
|
31
|
+
search: { running: "Recalling", done: "Recalled" }
|
|
32
|
+
},
|
|
33
|
+
{ running: "Working with memory", done: "Worked with memory" }
|
|
34
|
+
)
|
|
35
|
+
};
|
|
36
|
+
function titleOf(titles, toolName) {
|
|
37
|
+
return Object.hasOwn(titles, toolName) ? titles[toolName] ?? toolName : toolName;
|
|
38
|
+
}
|
|
39
|
+
function withMemoryToolTitles(labels, titles = memoryToolTitles) {
|
|
40
|
+
return {
|
|
41
|
+
...labels,
|
|
42
|
+
toolRunning: (toolName) => labels.toolRunning(titleOf(titles, toolName)),
|
|
43
|
+
toolDone: (toolName) => labels.toolDone(titleOf(titles, toolName)),
|
|
44
|
+
toolError: (toolName) => labels.toolError(titleOf(titles, toolName))
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
memoryToolTitles,
|
|
50
|
+
englishMemoryToolTitles,
|
|
51
|
+
memoryStepPhrases,
|
|
52
|
+
englishMemoryStepPhrases,
|
|
53
|
+
withMemoryToolTitles
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=chunk-VOET63ER.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tool-titles.ts"],"sourcesContent":["/** Названия строк вызова инструментов памяти для ленты: по-человечески, без имени инструмента. Без импортов: файл безопасно подключать в браузерной сборке. */\nexport const memoryToolTitles: Record<string, string> = {\n manage_memory: \"Работа с памятью\",\n};\n\n/** Та же подпись по-английски. */\nexport const englishMemoryToolTitles: Record<string, string> = {\n manage_memory: \"Working with memory\",\n};\n\ntype MemoryPhrase = { running: string; done: string };\ntype MemoryAction = \"create\" | \"update\" | \"delete\" | \"search\";\n\n/** Фраза памяти по действию из входа; входа нет (права, вход ещё пишется) — общая. */\nfunction memoryPhrase(byAction: Record<MemoryAction, MemoryPhrase>, general: MemoryPhrase) {\n return ({ input }: { input?: unknown }): MemoryPhrase => {\n const action = typeof input === \"object\" && input !== null ? (input as { action?: unknown }).action : undefined;\n return typeof action === \"string\" && Object.hasOwn(byAction, action) ? byAction[action as MemoryAction] : general;\n };\n}\n\n/** Понятные фразы памяти для простого вида чата (`stepPhrases` у `AgentChat`): по действию — запомнить, вспомнить… */\nexport const memoryStepPhrases: Record<string, (call: { input?: unknown }) => MemoryPhrase> = {\n manage_memory: memoryPhrase(\n {\n create: { running: \"Запоминаю\", done: \"Запомнил\" },\n update: { running: \"Обновляю память\", done: \"Обновил память\" },\n delete: { running: \"Забываю лишнее\", done: \"Забыл лишнее\" },\n search: { running: \"Вспоминаю\", done: \"Вспомнил\" },\n },\n { running: \"Работаю с памятью\", done: \"Поработал с памятью\" },\n ),\n};\n\n/** Те же фразы по-английски. */\nexport const englishMemoryStepPhrases: Record<string, (call: { input?: unknown }) => MemoryPhrase> = {\n manage_memory: memoryPhrase(\n {\n create: { running: \"Remembering\", done: \"Remembered\" },\n update: { running: \"Updating memory\", done: \"Updated memory\" },\n delete: { running: \"Forgetting what’s outdated\", done: \"Forgot what’s outdated\" },\n search: { running: \"Recalling\", done: \"Recalled\" },\n },\n { running: \"Working with memory\", done: \"Worked with memory\" },\n ),\n};\n\ninterface ToolLabels {\n toolRunning(toolName: string): string;\n toolDone(toolName: string): string;\n toolError(toolName: string): string;\n}\n\nfunction titleOf(titles: Record<string, string>, toolName: string): string {\n return Object.hasOwn(titles, toolName) ? (titles[toolName] ?? toolName) : toolName;\n}\n\n/** Надписи чата, в которых инструмент памяти назван по-русски (или по словарю `titles`, например `englishMemoryToolTitles`); остальное не меняется. */\nexport function withMemoryToolTitles<Labels extends ToolLabels>(labels: Labels, titles: Record<string, string> = memoryToolTitles): Labels {\n return {\n ...labels,\n toolRunning: (toolName: string) => labels.toolRunning(titleOf(titles, toolName)),\n toolDone: (toolName: string) => labels.toolDone(titleOf(titles, toolName)),\n toolError: (toolName: string) => labels.toolError(titleOf(titles, toolName)),\n };\n}\n"],"mappings":";AACO,IAAM,mBAA2C;AAAA,EACtD,eAAe;AACjB;AAGO,IAAM,0BAAkD;AAAA,EAC7D,eAAe;AACjB;AAMA,SAAS,aAAa,UAA8C,SAAuB;AACzF,SAAO,CAAC,EAAE,MAAM,MAAyC;AACvD,UAAM,SAAS,OAAO,UAAU,YAAY,UAAU,OAAQ,MAA+B,SAAS;AACtG,WAAO,OAAO,WAAW,YAAY,OAAO,OAAO,UAAU,MAAM,IAAI,SAAS,MAAsB,IAAI;AAAA,EAC5G;AACF;AAGO,IAAM,oBAAiF;AAAA,EAC5F,eAAe;AAAA,IACb;AAAA,MACE,QAAQ,EAAE,SAAS,0DAAa,MAAM,mDAAW;AAAA,MACjD,QAAQ,EAAE,SAAS,yFAAmB,MAAM,kFAAiB;AAAA,MAC7D,QAAQ,EAAE,SAAS,mFAAkB,MAAM,sEAAe;AAAA,MAC1D,QAAQ,EAAE,SAAS,0DAAa,MAAM,mDAAW;AAAA,IACnD;AAAA,IACA,EAAE,SAAS,gGAAqB,MAAM,2GAAsB;AAAA,EAC9D;AACF;AAGO,IAAM,2BAAwF;AAAA,EACnG,eAAe;AAAA,IACb;AAAA,MACE,QAAQ,EAAE,SAAS,eAAe,MAAM,aAAa;AAAA,MACrD,QAAQ,EAAE,SAAS,mBAAmB,MAAM,iBAAiB;AAAA,MAC7D,QAAQ,EAAE,SAAS,mCAA8B,MAAM,8BAAyB;AAAA,MAChF,QAAQ,EAAE,SAAS,aAAa,MAAM,WAAW;AAAA,IACnD;AAAA,IACA,EAAE,SAAS,uBAAuB,MAAM,qBAAqB;AAAA,EAC/D;AACF;AAQA,SAAS,QAAQ,QAAgC,UAA0B;AACzE,SAAO,OAAO,OAAO,QAAQ,QAAQ,IAAK,OAAO,QAAQ,KAAK,WAAY;AAC5E;AAGO,SAAS,qBAAgD,QAAgB,SAAiC,kBAA0B;AACzI,SAAO;AAAA,IACL,GAAG;AAAA,IACH,aAAa,CAAC,aAAqB,OAAO,YAAY,QAAQ,QAAQ,QAAQ,CAAC;AAAA,IAC/E,UAAU,CAAC,aAAqB,OAAO,SAAS,QAAQ,QAAQ,QAAQ,CAAC;AAAA,IACzE,WAAW,CAAC,aAAqB,OAAO,UAAU,QAAQ,QAAQ,QAAQ,CAAC;AAAA,EAC7E;AACF;","names":[]}
|