feat: add slash action tags, topic reference tool, and command bus system (#12860)

*  feat: add slash action tags in chat input

Made-with: Cursor

*  feat: enhance editor with new slash actions and localization updates

- Added new slash actions: change tone, condense, expand, polish, rewrite, summarize, and translate.
- Updated localization files for English and Chinese to include new action tags and slash commands.
- Removed deprecated useSlashItems component and integrated its functionality directly into InputEditor.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: add slash placement configuration to chat input components

- Introduced `slashPlacement` prop to `ChatInputProvider`, `StoreUpdater`, and `InputEditor` for customizable slash menu positioning.
- Updated initial state to include `slashPlacement` with default value 'top'.
- Adjusted `ChatInput` and `InputArea` components to utilize the new `slashPlacement` prop.

This enhancement allows for better control over the user interface in chat input interactions.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: implement command bus for slash action tags processing

Add command bus system to parse and execute slash commands (compact context,
new topic). Refactor action tag categories from ai/prompt to command/skill.
Add useEnabledSkills hook for dynamic skill registration.

* feat: compress command

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: compress

Signed-off-by: Innei <tukon479@gmail.com>

* fix: skill inject

*  feat: slash action tags with context engine integration

Made-with: Cursor

*  feat: add topic reference builtin tool and server runtime

Made-with: Cursor

*  feat: add topic mention items and update ReferTopic integration

Made-with: Cursor

* 🐛 fix: preserve editorData through assistant-group edit flow and update RichTextMessage reactively

- EditState now forwards editorData from EditorModal to modifyMessageContent
- modifyMessageContent accepts and passes editorData to updateMessageContent
- RichTextMessage uses useEditor + effect to update document on content change instead of key-based remount
- Refactored RichTextMessage plugins to use shared createChatInputRichPlugins()

*  feat(context-engine): add metadata types and update processors/providers

Made-with: Cursor

*  feat(chat-input): add slash action tags and restore failed input state

* 🔧 chore: update package dependencies and enhance Vite configuration

- Changed @lobehub/ui dependency to a specific package URL.
- Added multiple SPA entry points and layout files to the Vite warmup configuration.
- Removed unused monorepo packages from sharedOptimizeDeps and added various dayjs locales for better localization support.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui dependency to version 5.4.0 in package.json

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: correct SkillsApiName.runSkill to activateSkill and update trimmed content assertions

* 🐛 fix: resolve type errors in context-engine tests and InputEditor slashPlacement

* 🐛 fix: update runSkill to activateSkill in conversationLifecycle test

* 🐛 fix: avoid regex backtracking in placeholder parser

*  feat(localization): add action tags and tooltips for slash commands across multiple languages

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: preserve file attachments when /newTopic has no text content

* cleanup

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2026-03-13 22:17:36 +08:00 committed by GitHub
parent d7bfd1b6c8
commit 4438b559e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
180 changed files with 6021 additions and 534 deletions

View file

@ -14,6 +14,9 @@ description: TypeScript code style and optimization guidelines. Use when writing
- Prefer `as const satisfies XyzInterface` over plain `as const`
- Prefer `@ts-expect-error` over `@ts-ignore` over `as any`
- Avoid meaningless null/undefined parameters; design strict function contracts
- Prefer ES module augmentation (`declare module '...'`) over `namespace`; do not introduce `namespace`-based extension patterns
- When a type needs extensibility, expose a small mergeable interface at the source type and let each feature/plugin augment it locally instead of centralizing all extension fields in one registry file
- For package-local extensibility patterns like `PipelineContext.metadata`, define the metadata fields next to the processor/provider/plugin that reads or writes them
## Async Patterns

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "أمر",
"actionTag.category.skill": "مهارة",
"actionTag.category.tool": "أداة",
"actionTag.tooltip.command": "يشغّل أمر الشرطة المائلة على جانب العميل قبل الإرسال.",
"actionTag.tooltip.skill": "يحمّل حزمة مهارات قابلة لإعادة الاستخدام لهذا الطلب.",
"actionTag.tooltip.tool": "يشير إلى أداة اختارها المستخدم صراحةً لهذا الطلب.",
"actions.expand.off": "طي",
"actions.expand.on": "توسيع",
"actions.typobar.off": "إخفاء شريط تنسيق النص",
@ -34,12 +40,17 @@
"modifier.acceptAll": "الاحتفاظ بالجميع",
"modifier.reject": "تراجع",
"modifier.rejectAll": "تراجع عن الكل",
"slash.compact": "ضغط السياق",
"slash.h1": "عنوان 1",
"slash.h2": "عنوان 2",
"slash.h3": "عنوان 3",
"slash.hr": "فاصل",
"slash.newTopic": "الإرسال في موضوع جديد",
"slash.rewrite": "إعادة الصياغة",
"slash.summarize": "تلخيص",
"slash.table": "جدول",
"slash.tex": "صيغة TeX",
"slash.translate": "ترجمة",
"table.delete": "حذف الجدول",
"table.deleteColumn": "حذف العمود",
"table.deleteRow": "حذف الصف",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "البحث عن المهارات",
"builtins.lobe-skills.title": "المهارات",
"builtins.lobe-tools.apiName.activateTools": "تفعيل الأدوات",
"builtins.lobe-topic-reference.apiName.getTopicContext": "الحصول على سياق الموضوع",
"builtins.lobe-topic-reference.title": "مرجع الموضوع",
"builtins.lobe-user-memory.apiName.addContextMemory": "إضافة ذاكرة السياق",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "إضافة ذاكرة الخبرة",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "إضافة ذاكرة الهوية",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Команда",
"actionTag.category.skill": "Умение",
"actionTag.category.tool": "Инструмент",
"actionTag.tooltip.command": "Изпълнява локална slash команда преди изпращане.",
"actionTag.tooltip.skill": "Зарежда многократно използваем пакет с умения за тази заявка.",
"actionTag.tooltip.tool": "Маркира инструмент, който потребителят е избрал изрично за тази заявка.",
"actions.expand.off": "Свий",
"actions.expand.on": "Разгъни",
"actions.typobar.off": "Скрий лентата с форматиране",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Запази всички",
"modifier.reject": "Отмени",
"modifier.rejectAll": "Отмени всички",
"slash.compact": "Компресирай контекста",
"slash.h1": "Заглавие 1",
"slash.h2": "Заглавие 2",
"slash.h3": "Заглавие 3",
"slash.hr": "Разделител",
"slash.newTopic": "Изпрати в нова тема",
"slash.rewrite": "Пренапиши",
"slash.summarize": "Обобщи",
"slash.table": "Таблица",
"slash.tex": "TeX формула",
"slash.translate": "Преведи",
"table.delete": "Изтрий таблицата",
"table.deleteColumn": "Изтрий колона",
"table.deleteRow": "Изтрий ред",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Търсене на умения",
"builtins.lobe-skills.title": "Умения",
"builtins.lobe-tools.apiName.activateTools": "Активиране на инструменти",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Вземи контекста на темата",
"builtins.lobe-topic-reference.title": "Препратка към тема",
"builtins.lobe-user-memory.apiName.addContextMemory": "Добавяне на контекстна памет",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Добавяне на памет за опит",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Добавяне на памет за идентичност",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Befehl",
"actionTag.category.skill": "Fähigkeit",
"actionTag.category.tool": "Werkzeug",
"actionTag.tooltip.command": "Führt vor dem Senden einen clientseitigen Slash-Befehl aus.",
"actionTag.tooltip.skill": "Lädt für diese Anfrage ein wiederverwendbares Fähigkeitspaket.",
"actionTag.tooltip.tool": "Markiert ein Werkzeug, das der Benutzer für diese Anfrage ausdrücklich ausgewählt hat.",
"actions.expand.off": "Einklappen",
"actions.expand.on": "Ausklappen",
"actions.typobar.off": "Formatierungsleiste ausblenden",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Alle beibehalten",
"modifier.reject": "Zurücksetzen",
"modifier.rejectAll": "Alle zurücksetzen",
"slash.compact": "Kontext komprimieren",
"slash.h1": "Überschrift 1",
"slash.h2": "Überschrift 2",
"slash.h3": "Überschrift 3",
"slash.hr": "Trennlinie",
"slash.newTopic": "In neuem Thema senden",
"slash.rewrite": "Umschreiben",
"slash.summarize": "Zusammenfassen",
"slash.table": "Tabelle",
"slash.tex": "TeX-Formel",
"slash.translate": "Übersetzen",
"table.delete": "Tabelle löschen",
"table.deleteColumn": "Spalte löschen",
"table.deleteRow": "Zeile löschen",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Fähigkeiten suchen",
"builtins.lobe-skills.title": "Fähigkeiten",
"builtins.lobe-tools.apiName.activateTools": "Werkzeuge aktivieren",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Themenkontext abrufen",
"builtins.lobe-topic-reference.title": "Themenreferenz",
"builtins.lobe-user-memory.apiName.addContextMemory": "Kontextgedächtnis hinzufügen",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Erfahrungsgedächtnis hinzufügen",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Identitätsgedächtnis hinzufügen",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Command",
"actionTag.category.skill": "Skill",
"actionTag.category.tool": "Tool",
"actionTag.tooltip.command": "Runs a client-side slash command before sending.",
"actionTag.tooltip.skill": "Loads a reusable skill package for this request.",
"actionTag.tooltip.tool": "Marks a tool the user explicitly selected for this request.",
"actions.expand.off": "Collapse",
"actions.expand.on": "Expand",
"actions.typobar.off": "Hide formatting toolbar",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Keep All",
"modifier.reject": "Revert",
"modifier.rejectAll": "Revert All",
"slash.compact": "Compact context",
"slash.h1": "Heading 1",
"slash.h2": "Heading 2",
"slash.h3": "Heading 3",
"slash.hr": "Divider",
"slash.newTopic": "Send in new topic",
"slash.rewrite": "Rewrite",
"slash.summarize": "Summarize",
"slash.table": "Table",
"slash.tex": "TeX Formula",
"slash.translate": "Translate",
"table.delete": "Delete table",
"table.deleteColumn": "Delete column",
"table.deleteRow": "Delete row",

View file

@ -202,6 +202,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Search Skills",
"builtins.lobe-skills.title": "Skills",
"builtins.lobe-tools.apiName.activateTools": "Activate Tools",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Get Topic Context",
"builtins.lobe-topic-reference.title": "Topic Reference",
"builtins.lobe-user-memory.apiName.addContextMemory": "Add context memory",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Add experience memory",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Add identity memory",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Comando",
"actionTag.category.skill": "Habilidad",
"actionTag.category.tool": "Herramienta",
"actionTag.tooltip.command": "Ejecuta un comando de barra en el cliente antes de enviar.",
"actionTag.tooltip.skill": "Carga un paquete de habilidades reutilizable para esta solicitud.",
"actionTag.tooltip.tool": "Marca una herramienta que el usuario seleccionó explícitamente para esta solicitud.",
"actions.expand.off": "Colapsar",
"actions.expand.on": "Expandir",
"actions.typobar.off": "Ocultar barra de formato",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Conservar todo",
"modifier.reject": "Revertir",
"modifier.rejectAll": "Revertir todo",
"slash.compact": "Compactar contexto",
"slash.h1": "Encabezado 1",
"slash.h2": "Encabezado 2",
"slash.h3": "Encabezado 3",
"slash.hr": "Separador",
"slash.newTopic": "Enviar en un tema nuevo",
"slash.rewrite": "Reescribir",
"slash.summarize": "Resumir",
"slash.table": "Tabla",
"slash.tex": "Fórmula TeX",
"slash.translate": "Traducir",
"table.delete": "Eliminar tabla",
"table.deleteColumn": "Eliminar columna",
"table.deleteRow": "Eliminar fila",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Buscar Habilidades",
"builtins.lobe-skills.title": "Habilidades",
"builtins.lobe-tools.apiName.activateTools": "Activar Herramientas",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Obtener contexto del tema",
"builtins.lobe-topic-reference.title": "Referencia de tema",
"builtins.lobe-user-memory.apiName.addContextMemory": "Agregar memoria de contexto",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Agregar memoria de experiencia",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Agregar memoria de identidad",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "دستور",
"actionTag.category.skill": "مهارت",
"actionTag.category.tool": "ابزار",
"actionTag.tooltip.command": "پیش از ارسال، یک دستور اسلش سمت کلاینت را اجرا می‌کند.",
"actionTag.tooltip.skill": "برای این درخواست، یک بسته مهارت قابل استفاده مجدد را بارگذاری می‌کند.",
"actionTag.tooltip.tool": "ابزاری را که کاربر به‌طور صریح برای این درخواست انتخاب کرده است علامت‌گذاری می‌کند.",
"actions.expand.off": "جمع کردن",
"actions.expand.on": "باز کردن",
"actions.typobar.off": "پنهان کردن نوار ابزار قالب‌بندی",
@ -34,12 +40,17 @@
"modifier.acceptAll": "نگه‌داشتن همه",
"modifier.reject": "بازگرداندن",
"modifier.rejectAll": "بازگرداندن همه",
"slash.compact": "فشرده‌سازی زمینه",
"slash.h1": "سرفصل ۱",
"slash.h2": "سرفصل ۲",
"slash.h3": "سرفصل ۳",
"slash.hr": "خط جداکننده",
"slash.newTopic": "ارسال در موضوع جدید",
"slash.rewrite": "بازنویسی",
"slash.summarize": "خلاصه‌سازی",
"slash.table": "جدول",
"slash.tex": "فرمول TeX",
"slash.translate": "ترجمه",
"table.delete": "حذف جدول",
"table.deleteColumn": "حذف ستون",
"table.deleteRow": "حذف ردیف",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "جستجوی مهارت‌ها",
"builtins.lobe-skills.title": "مهارت‌ها",
"builtins.lobe-tools.apiName.activateTools": "فعال کردن ابزارها",
"builtins.lobe-topic-reference.apiName.getTopicContext": "دریافت زمینه موضوع",
"builtins.lobe-topic-reference.title": "ارجاع به موضوع",
"builtins.lobe-user-memory.apiName.addContextMemory": "افزودن حافظه زمینه",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "افزودن حافظه تجربه",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "افزودن حافظه هویتی",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Commande",
"actionTag.category.skill": "Compétence",
"actionTag.category.tool": "Outil",
"actionTag.tooltip.command": "Exécute une commande slash côté client avant lenvoi.",
"actionTag.tooltip.skill": "Charge un paquet de compétences réutilisable pour cette requête.",
"actionTag.tooltip.tool": "Marque un outil que lutilisateur a explicitement sélectionné pour cette requête.",
"actions.expand.off": "Réduire",
"actions.expand.on": "Développer",
"actions.typobar.off": "Masquer la barre de mise en forme",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Tout conserver",
"modifier.reject": "Annuler",
"modifier.rejectAll": "Tout annuler",
"slash.compact": "Compacter le contexte",
"slash.h1": "Titre 1",
"slash.h2": "Titre 2",
"slash.h3": "Titre 3",
"slash.hr": "Séparateur",
"slash.newTopic": "Envoyer dans un nouveau sujet",
"slash.rewrite": "Réécrire",
"slash.summarize": "Résumer",
"slash.table": "Tableau",
"slash.tex": "Formule TeX",
"slash.translate": "Traduire",
"table.delete": "Supprimer le tableau",
"table.deleteColumn": "Supprimer la colonne",
"table.deleteRow": "Supprimer la ligne",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Rechercher des Compétences",
"builtins.lobe-skills.title": "Compétences",
"builtins.lobe-tools.apiName.activateTools": "Activer les Outils",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Obtenir le contexte du sujet",
"builtins.lobe-topic-reference.title": "Référence de sujet",
"builtins.lobe-user-memory.apiName.addContextMemory": "Ajouter une mémoire de contexte",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Ajouter une mémoire d'expérience",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Ajouter une mémoire d'identité",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Comando",
"actionTag.category.skill": "Abilità",
"actionTag.category.tool": "Strumento",
"actionTag.tooltip.command": "Esegue un comando slash lato client prima dell'invio.",
"actionTag.tooltip.skill": "Carica un pacchetto di abilità riutilizzabile per questa richiesta.",
"actionTag.tooltip.tool": "Contrassegna uno strumento selezionato esplicitamente dall'utente per questa richiesta.",
"actions.expand.off": "Comprimi",
"actions.expand.on": "Espandi",
"actions.typobar.off": "Nascondi barra di formattazione",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Mantieni tutto",
"modifier.reject": "Ripristina",
"modifier.rejectAll": "Ripristina tutto",
"slash.compact": "Compatta il contesto",
"slash.h1": "Titolo 1",
"slash.h2": "Titolo 2",
"slash.h3": "Titolo 3",
"slash.hr": "Separatore",
"slash.newTopic": "Invia in un nuovo argomento",
"slash.rewrite": "Riscrivi",
"slash.summarize": "Riassumi",
"slash.table": "Tabella",
"slash.tex": "Formula TeX",
"slash.translate": "Traduci",
"table.delete": "Elimina tabella",
"table.deleteColumn": "Elimina colonna",
"table.deleteRow": "Elimina riga",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Cerca Abilità",
"builtins.lobe-skills.title": "Abilità",
"builtins.lobe-tools.apiName.activateTools": "Attiva Strumenti",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Ottieni contesto dell'argomento",
"builtins.lobe-topic-reference.title": "Riferimento argomento",
"builtins.lobe-user-memory.apiName.addContextMemory": "Aggiungi memoria contestuale",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Aggiungi memoria esperienziale",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Aggiungi memoria identità",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "コマンド",
"actionTag.category.skill": "スキル",
"actionTag.category.tool": "ツール",
"actionTag.tooltip.command": "送信前にクライアント側のスラッシュコマンドを実行します。",
"actionTag.tooltip.skill": "このリクエスト用に再利用可能なスキルパッケージを読み込みます。",
"actionTag.tooltip.tool": "このリクエストでユーザーが明示的に選択したツールを示します。",
"actions.expand.off": "折りたたむ",
"actions.expand.on": "展開する",
"actions.typobar.off": "書式ツールバーを非表示",
@ -34,12 +40,17 @@
"modifier.acceptAll": "すべて承諾する",
"modifier.reject": "取り消し",
"modifier.rejectAll": "すべてを取り消す",
"slash.compact": "コンテキストを圧縮",
"slash.h1": "見出し1",
"slash.h2": "見出し2",
"slash.h3": "見出し3",
"slash.hr": "区切り線",
"slash.newTopic": "新しいトピックで送信",
"slash.rewrite": "書き直す",
"slash.summarize": "要約",
"slash.table": "表",
"slash.tex": "TeX 数式",
"slash.translate": "翻訳",
"table.delete": "表を削除",
"table.deleteColumn": "列を削除",
"table.deleteRow": "行を削除",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "スキルを検索",
"builtins.lobe-skills.title": "スキル",
"builtins.lobe-tools.apiName.activateTools": "ツールをアクティブ化",
"builtins.lobe-topic-reference.apiName.getTopicContext": "トピックコンテキストを取得",
"builtins.lobe-topic-reference.title": "トピック参照",
"builtins.lobe-user-memory.apiName.addContextMemory": "コンテキスト記憶を追加",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "経験記憶を追加",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "アイデンティティ記憶を追加",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "명령",
"actionTag.category.skill": "스킬",
"actionTag.category.tool": "도구",
"actionTag.tooltip.command": "전송 전에 클라이언트 측 슬래시 명령을 실행합니다.",
"actionTag.tooltip.skill": "이번 요청에 사용할 재사용 가능한 스킬 패키지를 불러옵니다.",
"actionTag.tooltip.tool": "이번 요청에서 사용자가 명시적으로 선택한 도구를 표시합니다.",
"actions.expand.off": "접기",
"actions.expand.on": "펼치기",
"actions.typobar.off": "서식 도구 모음 숨기기",
@ -34,12 +40,17 @@
"modifier.acceptAll": "모두 수락",
"modifier.reject": "취소",
"modifier.rejectAll": "모두 취소",
"slash.compact": "컨텍스트 압축",
"slash.h1": "제목 1",
"slash.h2": "제목 2",
"slash.h3": "제목 3",
"slash.hr": "구분선",
"slash.newTopic": "새 토픽으로 보내기",
"slash.rewrite": "다시 쓰기",
"slash.summarize": "요약",
"slash.table": "표",
"slash.tex": "TeX 수식",
"slash.translate": "번역",
"table.delete": "표 삭제",
"table.deleteColumn": "열 삭제",
"table.deleteRow": "행 삭제",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "스킬 검색",
"builtins.lobe-skills.title": "스킬",
"builtins.lobe-tools.apiName.activateTools": "도구 활성화",
"builtins.lobe-topic-reference.apiName.getTopicContext": "토픽 컨텍스트 가져오기",
"builtins.lobe-topic-reference.title": "토픽 참조",
"builtins.lobe-user-memory.apiName.addContextMemory": "상황 기억 추가",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "경험 기억 추가",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "신원 기억 추가",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Commando",
"actionTag.category.skill": "Vaardigheid",
"actionTag.category.tool": "Hulpmiddel",
"actionTag.tooltip.command": "Voert vóór het verzenden een slash-commando aan de clientzijde uit.",
"actionTag.tooltip.skill": "Laadt voor dit verzoek een herbruikbaar vaardigheidspakket.",
"actionTag.tooltip.tool": "Markeert een hulpmiddel dat de gebruiker expliciet voor dit verzoek heeft geselecteerd.",
"actions.expand.off": "Samenvouwen",
"actions.expand.on": "Uitvouwen",
"actions.typobar.off": "Opmaakwerkbalk verbergen",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Alles behouden",
"modifier.reject": "Ongedaan maken",
"modifier.rejectAll": "Alles ongedaan maken",
"slash.compact": "Context comprimeren",
"slash.h1": "Kop 1",
"slash.h2": "Kop 2",
"slash.h3": "Kop 3",
"slash.hr": "Scheiding",
"slash.newTopic": "Verzenden in nieuw onderwerp",
"slash.rewrite": "Herschrijven",
"slash.summarize": "Samenvatten",
"slash.table": "Tabel",
"slash.tex": "TeX-formule",
"slash.translate": "Vertalen",
"table.delete": "Tabel verwijderen",
"table.deleteColumn": "Kolom verwijderen",
"table.deleteRow": "Rij verwijderen",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Vaardigheden Zoeken",
"builtins.lobe-skills.title": "Vaardigheden",
"builtins.lobe-tools.apiName.activateTools": "Hulpmiddelen Activeren",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Onderwerpcontext ophalen",
"builtins.lobe-topic-reference.title": "Onderwerpverwijzing",
"builtins.lobe-user-memory.apiName.addContextMemory": "Contextgeheugen toevoegen",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Ervaringsgeheugen toevoegen",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Identiteitsgeheugen toevoegen",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Polecenie",
"actionTag.category.skill": "Umiejętność",
"actionTag.category.tool": "Narzędzie",
"actionTag.tooltip.command": "Uruchamia polecenie slash po stronie klienta przed wysłaniem.",
"actionTag.tooltip.skill": "Wczytuje pakiet umiejętności wielokrotnego użytku dla tego żądania.",
"actionTag.tooltip.tool": "Oznacza narzędzie, które użytkownik jawnie wybrał dla tego żądania.",
"actions.expand.off": "Zwiń",
"actions.expand.on": "Rozwiń",
"actions.typobar.off": "Ukryj pasek formatowania",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Zachowaj wszystkie",
"modifier.reject": "Cofnij",
"modifier.rejectAll": "Cofnij wszystkie",
"slash.compact": "Kompresuj kontekst",
"slash.h1": "Nagłówek 1",
"slash.h2": "Nagłówek 2",
"slash.h3": "Nagłówek 3",
"slash.hr": "Separator",
"slash.newTopic": "Wyślij w nowym temacie",
"slash.rewrite": "Przepisz",
"slash.summarize": "Podsumuj",
"slash.table": "Tabela",
"slash.tex": "Formuła TeX",
"slash.translate": "Tłumacz",
"table.delete": "Usuń tabelę",
"table.deleteColumn": "Usuń kolumnę",
"table.deleteRow": "Usuń wiersz",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Wyszukaj Umiejętności",
"builtins.lobe-skills.title": "Umiejętności",
"builtins.lobe-tools.apiName.activateTools": "Aktywuj Narzędzia",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Pobierz kontekst tematu",
"builtins.lobe-topic-reference.title": "Odwołanie do tematu",
"builtins.lobe-user-memory.apiName.addContextMemory": "Dodaj pamięć kontekstu",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Dodaj pamięć doświadczenia",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Dodaj pamięć tożsamości",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Comando",
"actionTag.category.skill": "Habilidade",
"actionTag.category.tool": "Ferramenta",
"actionTag.tooltip.command": "Executa um comando slash no cliente antes do envio.",
"actionTag.tooltip.skill": "Carrega um pacote de habilidades reutilizável para esta solicitação.",
"actionTag.tooltip.tool": "Marca uma ferramenta que o usuário selecionou explicitamente para esta solicitação.",
"actions.expand.off": "Recolher",
"actions.expand.on": "Expandir",
"actions.typobar.off": "Ocultar barra de formatação",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Manter tudo",
"modifier.reject": "Reverter",
"modifier.rejectAll": "Reverter tudo",
"slash.compact": "Compactar contexto",
"slash.h1": "Título 1",
"slash.h2": "Título 2",
"slash.h3": "Título 3",
"slash.hr": "Divisor",
"slash.newTopic": "Enviar em novo tópico",
"slash.rewrite": "Reescrever",
"slash.summarize": "Resumir",
"slash.table": "Tabela",
"slash.tex": "Fórmula TeX",
"slash.translate": "Traduzir",
"table.delete": "Excluir tabela",
"table.deleteColumn": "Excluir coluna",
"table.deleteRow": "Excluir linha",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Buscar Habilidades",
"builtins.lobe-skills.title": "Habilidades",
"builtins.lobe-tools.apiName.activateTools": "Ativar Ferramentas",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Obter contexto do tópico",
"builtins.lobe-topic-reference.title": "Referência de tópico",
"builtins.lobe-user-memory.apiName.addContextMemory": "Adicionar memória de contexto",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Adicionar memória de experiência",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Adicionar memória de identidade",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Команда",
"actionTag.category.skill": "Навык",
"actionTag.category.tool": "Инструмент",
"actionTag.tooltip.command": "Выполняет клиентскую slash-команду перед отправкой.",
"actionTag.tooltip.skill": "Загружает для этого запроса переиспользуемый пакет навыков.",
"actionTag.tooltip.tool": "Помечает инструмент, который пользователь явно выбрал для этого запроса.",
"actions.expand.off": "Свернуть",
"actions.expand.on": "Развернуть",
"actions.typobar.off": "Скрыть панель форматирования",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Оставить все",
"modifier.reject": "Отменить",
"modifier.rejectAll": "Отменить все",
"slash.compact": "Сжать контекст",
"slash.h1": "Заголовок 1",
"slash.h2": "Заголовок 2",
"slash.h3": "Заголовок 3",
"slash.hr": "Разделитель",
"slash.newTopic": "Отправить в новую тему",
"slash.rewrite": "Переписать",
"slash.summarize": "Суммировать",
"slash.table": "Таблица",
"slash.tex": "Формула TeX",
"slash.translate": "Перевести",
"table.delete": "Удалить таблицу",
"table.deleteColumn": "Удалить столбец",
"table.deleteRow": "Удалить строку",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Поиск навыков",
"builtins.lobe-skills.title": "Навыки",
"builtins.lobe-tools.apiName.activateTools": "Активировать инструменты",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Получить контекст темы",
"builtins.lobe-topic-reference.title": "Ссылка на тему",
"builtins.lobe-user-memory.apiName.addContextMemory": "Добавить контекстную память",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Добавить память опыта",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Добавить память личности",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Komut",
"actionTag.category.skill": "Beceri",
"actionTag.category.tool": "Araç",
"actionTag.tooltip.command": "Göndermeden önce istemci tarafında bir slash komutu çalıştırır.",
"actionTag.tooltip.skill": "Bu istek için yeniden kullanılabilir bir beceri paketini yükler.",
"actionTag.tooltip.tool": "Kullanıcının bu istek için açıkça seçtiği bir aracı işaretler.",
"actions.expand.off": "Daralt",
"actions.expand.on": "Genişlet",
"actions.typobar.off": "Biçimlendirme araç çubuğunu gizle",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Tümünü koru",
"modifier.reject": "Geri al",
"modifier.rejectAll": "Tümünü geri al",
"slash.compact": "Bağlamı sıkıştır",
"slash.h1": "Başlık 1",
"slash.h2": "Başlık 2",
"slash.h3": "Başlık 3",
"slash.hr": "Ayraç",
"slash.newTopic": "Yeni konuda gönder",
"slash.rewrite": "Yeniden yaz",
"slash.summarize": "Özetle",
"slash.table": "Tablo",
"slash.tex": "TeX Formülü",
"slash.translate": "Çevir",
"table.delete": "Tabloyu sil",
"table.deleteColumn": "Sütunu sil",
"table.deleteRow": "Satırı sil",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Becerileri Ara",
"builtins.lobe-skills.title": "Beceriler",
"builtins.lobe-tools.apiName.activateTools": "Araçları Etkinleştir",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Konu bağlamını al",
"builtins.lobe-topic-reference.title": "Konu referansı",
"builtins.lobe-user-memory.apiName.addContextMemory": "Bağlam hafızası ekle",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Deneyim hafızası ekle",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Kimlik hafızası ekle",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "Lệnh",
"actionTag.category.skill": "Kỹ năng",
"actionTag.category.tool": "Công cụ",
"actionTag.tooltip.command": "Chạy lệnh slash phía client trước khi gửi.",
"actionTag.tooltip.skill": "Tải gói kỹ năng có thể tái sử dụng cho yêu cầu này.",
"actionTag.tooltip.tool": "Đánh dấu công cụ mà người dùng đã chọn rõ ràng cho yêu cầu này.",
"actions.expand.off": "Thu gọn",
"actions.expand.on": "Mở rộng",
"actions.typobar.off": "Ẩn thanh công cụ định dạng",
@ -34,12 +40,17 @@
"modifier.acceptAll": "Giữ tất cả",
"modifier.reject": "Hoàn tác",
"modifier.rejectAll": "Hoàn tác tất cả",
"slash.compact": "Nén ngữ cảnh",
"slash.h1": "Tiêu đề 1",
"slash.h2": "Tiêu đề 2",
"slash.h3": "Tiêu đề 3",
"slash.hr": "Đường phân cách",
"slash.newTopic": "Gửi trong chủ đề mới",
"slash.rewrite": "Viết lại",
"slash.summarize": "Tóm tắt",
"slash.table": "Bảng",
"slash.tex": "Công thức TeX",
"slash.translate": "Dịch",
"table.delete": "Xóa bảng",
"table.deleteColumn": "Xóa cột",
"table.deleteRow": "Xóa hàng",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "Tìm kiếm Kỹ năng",
"builtins.lobe-skills.title": "Kỹ năng",
"builtins.lobe-tools.apiName.activateTools": "Kích hoạt Công cụ",
"builtins.lobe-topic-reference.apiName.getTopicContext": "Lấy ngữ cảnh chủ đề",
"builtins.lobe-topic-reference.title": "Tham chiếu chủ đề",
"builtins.lobe-user-memory.apiName.addContextMemory": "Thêm trí nhớ ngữ cảnh",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "Thêm trí nhớ kinh nghiệm",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "Thêm trí nhớ danh tính",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "指令",
"actionTag.category.skill": "技能",
"actionTag.category.tool": "工具",
"actionTag.tooltip.command": "发送前会先在客户端执行这条 slash 指令。",
"actionTag.tooltip.skill": "表示这是一条可为本次请求加载的技能包。",
"actionTag.tooltip.tool": "表示这是用户为本次请求显式选中的工具。",
"actions.expand.off": "收起",
"actions.expand.on": "展开",
"actions.typobar.off": "隐藏格式工具栏",
@ -34,12 +40,17 @@
"modifier.acceptAll": "全部保留",
"modifier.reject": "撤销",
"modifier.rejectAll": "全部撤销",
"slash.compact": "压缩上下文",
"slash.h1": "一级标题",
"slash.h2": "二级标题",
"slash.h3": "三级标题",
"slash.hr": "分割线",
"slash.newTopic": "在新话题中发送",
"slash.rewrite": "重写",
"slash.summarize": "总结",
"slash.table": "表格",
"slash.tex": "TeX 公式",
"slash.translate": "翻译",
"table.delete": "删除表格",
"table.deleteColumn": "删除列",
"table.deleteRow": "删除行",

View file

@ -202,6 +202,8 @@
"builtins.lobe-skills.apiName.searchSkill": "搜索技能",
"builtins.lobe-skills.title": "技能",
"builtins.lobe-tools.apiName.activateTools": "激活工具",
"builtins.lobe-topic-reference.apiName.getTopicContext": "获取话题上下文",
"builtins.lobe-topic-reference.title": "话题引用",
"builtins.lobe-user-memory.apiName.addContextMemory": "添加情境记忆",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "添加经验记忆",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "添加身份记忆",

View file

@ -1,4 +1,10 @@
{
"actionTag.category.command": "命令",
"actionTag.category.skill": "技能",
"actionTag.category.tool": "工具",
"actionTag.tooltip.command": "在發送前執行客戶端斜線命令。",
"actionTag.tooltip.skill": "為此次請求載入可重用的技能包。",
"actionTag.tooltip.tool": "標記使用者在此次請求中明確選擇的工具。",
"actions.expand.off": "收合",
"actions.expand.on": "展開",
"actions.typobar.off": "隱藏格式工具列",
@ -34,12 +40,17 @@
"modifier.acceptAll": "全部接受",
"modifier.reject": "撤銷",
"modifier.rejectAll": "全部取消",
"slash.compact": "壓縮上下文",
"slash.h1": "一級標題",
"slash.h2": "二級標題",
"slash.h3": "三級標題",
"slash.hr": "分隔線",
"slash.newTopic": "在新話題中發送",
"slash.rewrite": "改寫",
"slash.summarize": "摘要",
"slash.table": "表格",
"slash.tex": "TeX 公式",
"slash.translate": "翻譯",
"table.delete": "刪除表格",
"table.deleteColumn": "刪除列",
"table.deleteRow": "刪除行",

View file

@ -201,6 +201,8 @@
"builtins.lobe-skills.apiName.searchSkill": "搜尋技能",
"builtins.lobe-skills.title": "技能",
"builtins.lobe-tools.apiName.activateTools": "啟用工具",
"builtins.lobe-topic-reference.apiName.getTopicContext": "取得話題上下文",
"builtins.lobe-topic-reference.title": "話題引用",
"builtins.lobe-user-memory.apiName.addContextMemory": "新增情境記憶",
"builtins.lobe-user-memory.apiName.addExperienceMemory": "新增經驗記憶",
"builtins.lobe-user-memory.apiName.addIdentityMemory": "新增身份記憶",

View file

@ -195,6 +195,7 @@
"@icons-pack/react-simple-icons": "^13.8.0",
"@khmyznikov/pwa-install": "0.3.9",
"@langchain/community": "^0.3.59",
"@lexical/utils": "^0.39.0",
"@lobechat/adapter-lark": "workspace:*",
"@lobechat/adapter-qq": "workspace:*",
"@lobechat/agent-runtime": "workspace:*",
@ -216,6 +217,7 @@
"@lobechat/builtin-tool-skill-store": "workspace:*",
"@lobechat/builtin-tool-skills": "workspace:*",
"@lobechat/builtin-tool-tools": "workspace:*",
"@lobechat/builtin-tool-topic-reference": "workspace:*",
"@lobechat/builtin-tool-web-browsing": "workspace:*",
"@lobechat/builtin-tools": "workspace:*",
"@lobechat/business-config": "workspace:*",
@ -312,6 +314,7 @@
"fflate": "^0.8.2",
"ffmpeg-static": "^5.3.0",
"file-type": "^21.3.0",
"fuse.js": "^7.0.0",
"gray-matter": "^4.0.3",
"html-to-text": "^9.0.5",
"i18next": "^25.8.0",
@ -326,6 +329,7 @@
"langchain": "^0.3.37",
"langfuse": "^3.38.6",
"langfuse-core": "^3.38.6",
"lexical": "^0.39.0",
"lucide-react": "^0.562.0",
"mammoth": "^1.11.0",
"marked": "^17.0.1",

View file

@ -67,8 +67,14 @@ class AgentManagementExecutor extends BaseExecutor<typeof AgentManagementApiName
params: CallAgentParams,
ctx: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
const { agentId, instruction, runAsTask, taskTitle, timeout, skipCallSupervisor = false } =
params;
const {
agentId,
instruction,
runAsTask,
taskTitle,
timeout,
skipCallSupervisor = false,
} = params;
if (runAsTask) {
// Execute as async task using GTD exec_task pattern
@ -184,17 +190,27 @@ class AgentManagementExecutor extends BaseExecutor<typeof AgentManagementApiName
return;
}
// If instruction is provided, inject it as a virtual User Message
// Same pattern as group orchestration's call_agent executor:
// virtual message with <speaker> tag gives the called agent clear direction
const now = Date.now();
const messagesWithInstruction = instruction
? [
...messages,
{
content: `<speaker name="Supervisor" />\n${instruction}`,
createdAt: now,
id: `virtual_speak_instruction_${now}`,
role: 'user' as const,
updatedAt: now,
},
]
: messages;
try {
// Execute with subAgentId + scope: 'sub_agent'
// - context.agentId = current agent (for message storage and message.agentId)
// - context.topicId = current topic
// - context.subAgentId = target agent (for agent config - model, prompt, etc.)
// - context.scope = 'sub_agent' (indicates this is agent-to-agent call, not group)
// This will create messages in current agent's conversation but use target agent's config
// The message.agentId will still be current agent, but metadata stores subAgentId + scope
await get().internal_execAgentRuntime({
context: { ...conversationContext, subAgentId: agentId, scope: 'sub_agent' },
messages,
messages: messagesWithInstruction,
parentMessageId: ctx.messageId,
parentMessageType: 'tool',
});

View file

@ -0,0 +1,13 @@
{
"name": "@lobechat/builtin-tool-topic-reference",
"version": "1.0.0",
"private": true,
"exports": {
".": "./src/index.ts",
"./executor": "./src/executor/index.ts"
},
"main": "./src/index.ts",
"devDependencies": {
"@lobechat/types": "workspace:*"
}
}

View file

@ -0,0 +1,40 @@
import type { BuiltinToolContext, BuiltinToolResult } from '@lobechat/types';
import { BaseExecutor } from '@lobechat/types';
import { lambdaClient } from '@/libs/trpc/client';
import { TopicReferenceIdentifier } from '../types';
const TopicReferenceApiName = {
getTopicContext: 'getTopicContext',
} as const;
interface GetTopicContextParams {
topicId: string;
}
class TopicReferenceExecutor extends BaseExecutor<typeof TopicReferenceApiName> {
readonly identifier = TopicReferenceIdentifier;
protected readonly apiEnum = TopicReferenceApiName;
getTopicContext = async (
params: GetTopicContextParams,
_ctx: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
const { topicId } = params;
if (!topicId) {
return { content: 'topicId is required', success: false };
}
try {
const result = await lambdaClient.topic.getTopicContext.query({ topicId });
return { content: result.content, success: result.success };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return { content: `Failed to fetch topic context: ${errorMessage}`, success: false };
}
};
}
export const topicReferenceExecutor = new TopicReferenceExecutor();

View file

@ -0,0 +1,6 @@
export { TopicReferenceManifest } from './manifest';
export {
TopicReferenceApiName,
type TopicReferenceApiNameType,
TopicReferenceIdentifier,
} from './types';

View file

@ -0,0 +1,32 @@
import { type BuiltinToolManifest } from '@lobechat/types';
import { TopicReferenceApiName, TopicReferenceIdentifier } from './types';
export const TopicReferenceManifest: BuiltinToolManifest = {
api: [
{
description:
'Retrieve context from a referenced topic conversation. Returns the topic summary if available, otherwise returns the most recent messages. Use this when you see a topic reference tag in the user message and need to understand what was discussed in that topic.',
name: TopicReferenceApiName.getTopicContext,
parameters: {
additionalProperties: false,
properties: {
topicId: {
description: 'The ID of the topic to retrieve context from',
type: 'string',
},
},
required: ['topicId'],
type: 'object',
},
},
],
identifier: TopicReferenceIdentifier,
meta: {
avatar: '📋',
description: 'Retrieve context from referenced topic conversations',
title: 'Topic Reference',
},
systemRole: '',
type: 'builtin',
};

View file

@ -0,0 +1,8 @@
export const TopicReferenceIdentifier = 'lobe-topic-reference';
export const TopicReferenceApiName = {
getTopicContext: 'getTopicContext',
} as const;
export type TopicReferenceApiNameType =
(typeof TopicReferenceApiName)[keyof typeof TopicReferenceApiName];

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"exclude": ["node_modules"],
"include": ["src/**/*"]
}

View file

@ -30,6 +30,7 @@
"@lobechat/builtin-tool-skill-store": "workspace:*",
"@lobechat/builtin-tool-skills": "workspace:*",
"@lobechat/builtin-tool-tools": "workspace:*",
"@lobechat/builtin-tool-topic-reference": "workspace:*",
"@lobechat/builtin-tool-web-browsing": "workspace:*",
"@lobechat/const": "workspace:*"
},

View file

@ -13,6 +13,7 @@ import { PageAgentManifest } from '@lobechat/builtin-tool-page-agent';
import { SkillStoreManifest } from '@lobechat/builtin-tool-skill-store';
import { SkillsManifest } from '@lobechat/builtin-tool-skills';
import { LobeToolsManifest } from '@lobechat/builtin-tool-tools';
import { TopicReferenceManifest } from '@lobechat/builtin-tool-topic-reference';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
export const builtinToolIdentifiers: string[] = [
@ -30,6 +31,7 @@ export const builtinToolIdentifiers: string[] = [
GTDManifest.identifier,
MemoryManifest.identifier,
NotebookManifest.identifier,
TopicReferenceManifest.identifier,
LobeToolsManifest.identifier,
SkillStoreManifest.identifier,
];

View file

@ -14,6 +14,7 @@ import { RemoteDeviceManifest } from '@lobechat/builtin-tool-remote-device';
import { SkillStoreManifest } from '@lobechat/builtin-tool-skill-store';
import { SkillsManifest } from '@lobechat/builtin-tool-skills';
import { LobeToolsManifest } from '@lobechat/builtin-tool-tools';
import { TopicReferenceManifest } from '@lobechat/builtin-tool-topic-reference';
import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing';
import { isDesktop } from '@lobechat/const';
import { type LobeBuiltinTool } from '@lobechat/types';
@ -31,6 +32,7 @@ export const defaultToolIds = [
MemoryManifest.identifier,
LocalSystemManifest.identifier,
CloudSandboxManifest.identifier,
TopicReferenceManifest.identifier,
];
/**
@ -147,4 +149,11 @@ export const builtinTools: LobeBuiltinTool[] = [
manifest: RemoteDeviceManifest,
type: 'builtin',
},
{
discoverable: false,
hidden: true,
identifier: TopicReferenceManifest.identifier,
manifest: TopicReferenceManifest,
type: 'builtin',
},
];

View file

@ -0,0 +1,49 @@
import { describe, expectTypeOf, it } from 'vitest';
import type { PipelineContext, PipelineContextMetadata } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
customMetadata?: {
enabled: boolean;
source: 'test';
};
}
}
describe('PipelineContextMetadata', () => {
it('should keep built-in metadata fields strongly typed', () => {
expectTypeOf<PipelineContextMetadata['historySummary']>().toEqualTypeOf<
| {
formattedLength: number;
injected: boolean;
originalLength: number;
}
| undefined
>();
expectTypeOf<PipelineContextMetadata['toolMessageReorder']>().toEqualTypeOf<
| {
originalCount: number;
removedInvalidTools: number;
reorderedCount: number;
}
| undefined
>();
});
it('should support template literal injected-count metadata keys', () => {
expectTypeOf<PipelineContextMetadata['CustomProviderInjectedCount']>().toEqualTypeOf<
number | undefined
>();
});
it('should expose consumer-side metadata extensions through pipeline context', () => {
expectTypeOf<PipelineContext['metadata']['customMetadata']>().toEqualTypeOf<
| {
enabled: boolean;
source: 'test';
}
| undefined
>();
});
});

View file

@ -176,9 +176,9 @@ describe('ContextEngine', () => {
expect(result.isAborted).toBe(false);
expect(result.messages).toEqual(input.messages);
expect(result.metadata.p1).toBe(true);
expect(result.metadata.p2).toBe(true);
expect(result.metadata.p3).toBe(true);
expect((result.metadata as any).p1).toBe(true);
expect((result.metadata as any).p2).toBe(true);
expect((result.metadata as any).p3).toBe(true);
expect(result.stats.processedCount).toBe(3);
});

View file

@ -185,14 +185,14 @@ describe('BaseProcessor', () => {
it('should preserve all context properties', async () => {
const processor = new TestProcessor();
const context = createContext([{ content: 'test', role: 'user' }]);
context.metadata.customField = 'customValue';
(context.metadata as any).customField = 'customValue';
const result = await processor.process(context);
expect(result.isAborted).toBe(context.isAborted);
expect(result.metadata.model).toBe(context.metadata.model);
expect(result.metadata.maxTokens).toBe(context.metadata.maxTokens);
expect(result.metadata.customField).toBe('customValue');
expect((result.metadata as any).customField).toBe('customValue');
});
});

View file

@ -35,6 +35,7 @@ import {
KnowledgeInjector,
PageEditorContextInjector,
PageSelectionsInjector,
SelectedSkillInjector,
SkillContextProvider,
SystemDateProvider,
SystemRoleInjector,
@ -129,6 +130,7 @@ export class MessagesEngine {
formatHistorySummary,
knowledge,
skillsConfig,
selectedSkills,
toolDiscoveryConfig,
toolsConfig,
capabilities,
@ -157,6 +159,8 @@ export class MessagesEngine {
const isGroupContextEnabled =
isAgentGroupEnabled || !!agentGroup?.currentAgentId || !!agentGroup?.members;
const isUserMemoryEnabled = userMemory?.enabled && userMemory?.memories;
const hasSelectedSkills = (selectedSkills?.length ?? 0) > 0;
// Page editor is enabled if either direct pageContentContext or initialContext.pageEditor is provided
const isPageEditorEnabled = !!pageContentContext || !!initialContext?.pageEditor;
// GTD is enabled if gtd.enabled is true and either plan or todos is provided
@ -283,16 +287,19 @@ export class MessagesEngine {
historySummary,
}),
// 14. Page Selections injection (inject user-selected text into each user message that has them)
// 14. Selected skill injection (ephemeral user-selected slash skills for this request)
...(hasSelectedSkills ? [new SelectedSkillInjector({ selectedSkills })] : []),
// 15. Page Selections injection (inject user-selected text into each user message that has them)
new PageSelectionsInjector({ enabled: isPageEditorEnabled }),
// 15. Page Editor context injection (inject current page content to last user message)
// 16. Page Editor context injection (inject current page content to last user message)
new PageEditorContextInjector({
enabled: isPageEditorEnabled,
// Use direct pageContentContext if provided (server-side), otherwise build from initialContext + stepContext (frontend)
pageContentContext: pageContentContext
? pageContentContext
: initialContext?.pageEditor
pageContentContext:
pageContentContext ??
(initialContext?.pageEditor
? {
markdown: initialContext.pageEditor.markdown,
metadata: {
@ -303,10 +310,10 @@ export class MessagesEngine {
// Use latest XML from stepContext if available, otherwise fallback to initial XML
xml: stepContext?.stepPageEditor?.xml || initialContext.pageEditor.xml,
}
: undefined,
: undefined),
}),
// 16. GTD Todo injection (conditionally added, at end of last user message)
// 17. GTD Todo injection (conditionally added, at end of last user message)
...(isGTDTodoEnabled ? [new GTDTodoInjector({ enabled: true, todos: gtd.todos })] : []),
// =============================================

View file

@ -1,6 +1,10 @@
/* eslint-disable perfectionist/sort-interfaces */
import type { FileContent, KnowledgeBaseInfo, PageContentContext } from '@lobechat/prompts';
import type { RuntimeInitialContext, RuntimeStepContext } from '@lobechat/types';
import type {
RuntimeInitialContext,
RuntimeSelectedSkill,
RuntimeStepContext,
} from '@lobechat/types';
import type { OpenAIChatMessage, UIChatMessage } from '@/types/index';
@ -15,6 +19,7 @@ import type { GTDPlan } from '../../providers/GTDPlanInjector';
import type { GTDTodoList } from '../../providers/GTDTodoInjector';
import type { SkillMeta } from '../../providers/SkillContextProvider';
import type { ToolDiscoveryMeta } from '../../providers/ToolDiscoveryProvider';
import type { PipelineContextMetadata } from '../../types';
import type { LobeToolManifest } from '../tools/types';
/**
@ -239,6 +244,8 @@ export interface MessagesEngineParams {
// ========== Skills ==========
/** Skills configuration */
skillsConfig?: SkillsConfig;
/** Skills explicitly selected by the user for the current request */
selectedSkills?: RuntimeSelectedSkill[];
// ========== Tool Discovery ==========
/** Tool Discovery configuration (available tools for dynamic activation) */
@ -299,7 +306,7 @@ export interface MessagesEngineResult {
/** Processed messages in OpenAI format */
messages: OpenAIChatMessage[];
/** Processing metadata */
metadata: Record<string, any>;
metadata: PipelineContextMetadata;
/** Processing statistics */
stats: {
/** Number of processors executed */

View file

@ -3,6 +3,15 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
agentCouncilAssistantMessagesCreated?: number;
agentCouncilFlattenProcessed?: number;
agentCouncilMessagesFlattened?: number;
agentCouncilToolMessagesCreated?: number;
}
}
const log = debug('context-engine:processor:AgentCouncilFlattenProcessor');
/**

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { Message, PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
compressedGroupRoleTransformProcessed?: number;
}
}
const log = debug('context-engine:processor:CompressedGroupRoleTransformProcessor');
/**

View file

@ -3,6 +3,15 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
assistantMessagesCreated?: number;
groupMessagesFlattened?: number;
groupMessagesFlattenProcessed?: number;
toolMessagesCreated?: number;
}
}
const log = debug('context-engine:processor:GroupMessageFlattenProcessor');
/**

View file

@ -3,6 +3,16 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { Message, PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
orchestrationFilterProcessed?: {
assistantFiltered: number;
filteredCount: number;
toolFiltered: number;
};
}
}
const log = debug('context-engine:processor:GroupOrchestrationFilterProcessor');
/**

View file

@ -3,6 +3,15 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { Message, PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
groupRoleTransformProcessed?: {
assistantTransformed: number;
toolTransformed: number;
};
}
}
const log = debug('context-engine:processor:GroupRoleTransformProcessor');
/**

View file

@ -3,6 +3,13 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
finalMessageCount?: number;
historyTruncated?: number;
}
}
const log = debug('context-engine:processor:HistoryTruncateProcessor');
export interface HistoryTruncateConfig {
@ -261,7 +268,11 @@ export const getSlicedMessages = (
// Step 2: Walk backwards through messages to select last N groups
const selectedGroupIndices = new Set<number>();
for (let i = messages.length - 1; i >= 0 && selectedGroupIndices.size < options.historyCount; i--) {
for (
let i = messages.length - 1;
i >= 0 && selectedGroupIndices.size < options.historyCount;
i--
) {
const msg = messages[i] as MinimalMessage;
const groupIdx = messageToGroup.get(msg.id);
if (groupIdx !== undefined) {

View file

@ -4,6 +4,12 @@ import { template } from 'es-toolkit/compat';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
inputTemplateProcessed?: number;
}
}
const log = debug('context-engine:processor:InputTemplateProcessor');
export interface InputTemplateConfig {
@ -39,7 +45,7 @@ export class InputTemplateProcessor extends BaseProcessor {
try {
// Compile the template
const compiler = template(this.config.inputTemplate, {
interpolate: /{{\s*(text)\s*}}/g,
interpolate: /\{\{\s*(text)\s*\}\}/g,
});
log(`Applying input template: ${this.config.inputTemplate}`);

View file

@ -3,6 +3,15 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
messageCleanup?: {
cleanedCount: number;
totalMessages: number;
};
}
}
const log = debug('context-engine:processor:MessageCleanupProcessor');
/**

View file

@ -8,6 +8,14 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
assistantMessagesProcessed?: number;
messageContentProcessed?: number;
userMessagesProcessed?: number;
}
}
const log = debug('context-engine:processor:MessageContentProcessor');
/**

View file

@ -3,9 +3,23 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
placeholderVariablesProcessed?: number;
}
}
const log = debug('context-engine:processor:PlaceholderVariablesProcessor');
const placeholderVariablesRegex = /{{(.*?)}}/g;
const PLACEHOLDER_START = '{{';
const PLACEHOLDER_END = '}}';
interface PlaceholderToken {
end: number;
key: string;
raw: string;
start: number;
}
export type PlaceholderValue = unknown | (() => unknown);
export type PlaceholderValueMap = Record<string, PlaceholderValue>;
@ -63,9 +77,56 @@ export interface PlaceholderVariablesConfig {
* @param text String containing template variables
* @returns Array of variable names, e.g. ['date', 'nickname']
*/
const extractPlaceholderVariables = (text: string): string[] => {
const matches = [...text.matchAll(placeholderVariablesRegex)];
return matches.map((m) => m[1].trim());
const extractPlaceholderTokens = (text: string): PlaceholderToken[] => {
const tokens: PlaceholderToken[] = [];
let searchIndex = 0;
while (searchIndex < text.length) {
const start = text.indexOf(PLACEHOLDER_START, searchIndex);
if (start === -1) break;
const end = text.indexOf(PLACEHOLDER_END, start + PLACEHOLDER_START.length);
if (end === -1) break;
const tokenEnd = end + PLACEHOLDER_END.length;
tokens.push({
end: tokenEnd,
key: text.slice(start + PLACEHOLDER_START.length, end).trim(),
raw: text.slice(start, tokenEnd),
start,
});
searchIndex = tokenEnd;
}
return tokens;
};
const replaceAvailablePlaceholders = (
text: string,
placeholders: PlaceholderToken[],
availableVariables: Record<string, string>,
): string => {
const output: string[] = [];
let cursor = 0;
let changed = false;
for (const placeholder of placeholders) {
output.push(text.slice(cursor, placeholder.start));
if (Object.hasOwn(availableVariables, placeholder.key)) {
output.push(availableVariables[placeholder.key]);
changed = true;
} else {
output.push(placeholder.raw);
}
cursor = placeholder.end;
}
output.push(text.slice(cursor));
return changed ? output.join('') : text;
};
/**
@ -85,14 +146,17 @@ export const parsePlaceholderVariables = (
// Recursive parsing to handle cases like {{text}} containing additional preset variables
for (let i = 0; i < depth; i++) {
try {
const extractedVariables = extractPlaceholderVariables(result);
const placeholders = extractPlaceholderTokens(result);
const extractedVariables = placeholders.map((placeholder) => placeholder.key);
if (placeholders.length === 0) break;
log('Extracted variables from text: %o', extractedVariables);
log('Available generator keys: %o', Object.keys(variableGenerators));
// Debug: check if text contains {{username}} pattern
if (result.includes('username') || result.includes('{{')) {
const matches = result.match(/{{[^}]*}}/g);
const matches = placeholders.map((placeholder) => placeholder.raw);
log('All {{...}} patterns found in text: %o', matches);
}
@ -112,16 +176,7 @@ export const parsePlaceholderVariables = (
// Only perform replacement when there are available variables
if (Object.keys(availableVariables).length === 0) break;
// Replace variables one by one to avoid es-toolkit template's error handling for undefined variables
let tempResult = result;
for (const [key, value] of Object.entries(availableVariables)) {
const regex = new RegExp(
`{{\\s*${key.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&')}\\s*}}`,
'g',
);
// @ts-ignore
tempResult = tempResult.replace(regex, value);
}
const tempResult = replaceAvailablePlaceholders(result, placeholders, availableVariables);
if (tempResult === result) break;
result = tempResult;

View file

@ -4,6 +4,12 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
reactionFeedbackProcessed?: number;
}
}
const log = debug('context-engine:processor:ReactionFeedbackProcessor');
export interface ReactionFeedbackConfig {

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { Message, PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
supervisorRoleRestoreProcessed?: number;
}
}
const log = debug('context-engine:processor:SupervisorRoleRestoreProcessor');
/**

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
taskMessagesProcessed?: number;
}
}
const log = debug('context-engine:processor:TaskMessageProcessor');
/**

View file

@ -3,6 +3,14 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
taskMessagesCreated?: number;
tasksFlattenProcessed?: number;
tasksMessagesFlattened?: number;
}
}
const log = debug('context-engine:processor:TasksFlattenProcessor');
/**

View file

@ -3,6 +3,15 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { MessageToolCall, PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
supportTools?: boolean;
toolCallProcessed?: number;
toolCallsConverted?: number;
toolMessagesConverted?: number;
}
}
const log = debug('context-engine:processor:ToolCallProcessor');
export interface ToolCallConfig {

View file

@ -3,6 +3,16 @@ import debug from 'debug';
import { BaseProcessor } from '../base/BaseProcessor';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
toolMessageReorder?: {
originalCount: number;
removedInvalidTools: number;
reorderedCount: number;
};
}
}
const log = debug('context-engine:processor:ToolMessageReorder');
/**

View file

@ -301,8 +301,8 @@ describe('MessageCleanupProcessor', () => {
const result = await processor.process(context);
// Both messages are "cleaned" because comparison is done by reference
expect(result.metadata.messageCleanup.cleanedCount).toBe(2);
expect(result.metadata.messageCleanup.totalMessages).toBe(2);
expect(result.metadata.messageCleanup!.cleanedCount).toBe(2);
expect(result.metadata.messageCleanup!.totalMessages).toBe(2);
});
it('should handle empty messages array', async () => {

View file

@ -53,6 +53,18 @@ describe('PlaceholderVariablesProcessor', () => {
const result = parsePlaceholderVariables(text, mockVariableGenerators);
expect(result).toBe('No placeholders here');
});
it('should replace placeholders with surrounding whitespace', () => {
const text = 'Hello {{ username }}, today is {{ date }}';
const result = parsePlaceholderVariables(text, mockVariableGenerators);
expect(result).toBe('Hello TestUser, today is 2023-12-25');
});
it('should handle malformed repeated opening braces without backtracking issues', () => {
const text = '{{{{'.repeat(2000);
const result = parsePlaceholderVariables(text, mockVariableGenerators);
expect(result).toBe(text);
});
});
describe('parsePlaceholderVariablesMessages', () => {

View file

@ -1,21 +1,16 @@
import { escapeXml } from '@lobechat/prompts';
import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
const log = debug('context-engine:provider:AgentBuilderContextInjector');
declare module '../types' {
interface PipelineContextMetadataOverrides {
agentBuilderContextInjected?: boolean;
}
}
/**
* Escape XML special characters
*/
const escapeXml = (str: string): string => {
return str
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
};
const log = debug('context-engine:provider:AgentBuilderContextInjector');
/**
* Official tool item for Agent Builder context

View file

@ -1,21 +1,17 @@
import { escapeXml } from '@lobechat/prompts';
import type { RuntimeMentionedAgent } from '@lobechat/types';
import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
const log = debug('context-engine:provider:AgentManagementContextInjector');
declare module '../types' {
interface PipelineContextMetadataOverrides {
agentManagementContextInjected?: boolean;
}
}
/**
* Escape XML special characters
*/
const escapeXml = (str: string): string => {
return str
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
};
const log = debug('context-engine:provider:AgentManagementContextInjector');
/**
* Available model info for Agent Management context
@ -70,6 +66,8 @@ export interface AgentManagementContext {
availablePlugins?: AvailablePluginInfo[];
/** Available providers and models */
availableProviders?: AvailableProviderInfo[];
/** Agents @mentioned by the user — supervisor should delegate to these via callAgent */
mentionedAgents?: RuntimeMentionedAgent[];
}
export interface AgentManagementContextInjectorConfig {
@ -166,6 +164,21 @@ ${parts.join('\n')}
</agent_management_context>`;
};
/**
* Format mentioned agents as delegation context for injection after the user message.
* Instructs the AI to delegate to the mentioned agent(s) via callAgent.
*/
const formatMentionedAgentsContext = (mentionedAgents: RuntimeMentionedAgent[]): string => {
const agentsXml = mentionedAgents
.map((a) => ` <agent id="${escapeXml(a.id)}" name="${escapeXml(a.name)}" />`)
.join('\n');
return `<mentioned_agents>
<instruction>The user has @mentioned the following agent(s) in their message. You MUST use the callAgent tool to delegate the user's request to the mentioned agent. Do NOT attempt to handle the request yourself call the agent and let them respond.</instruction>
${agentsXml}
</mentioned_agents>`;
};
/**
* Agent Management Context Injector
* Responsible for injecting available models and plugins when Agent Management tool is enabled
@ -195,41 +208,70 @@ export class AgentManagementContextInjector extends BaseProvider {
return this.markAsExecuted(clonedContext);
}
// Format context
const hasMentionedAgents =
this.config.context.mentionedAgents && this.config.context.mentionedAgents.length > 0;
// Format context (excluding mentionedAgents — those are injected separately after the last user message)
const contextWithoutMentions: AgentManagementContext = hasMentionedAgents
? {
availablePlugins: this.config.context.availablePlugins,
availableProviders: this.config.context.availableProviders,
}
: this.config.context;
const formatFn = this.config.formatContext || defaultFormatContext;
const formattedContent = formatFn(this.config.context);
const formattedContent = formatFn(contextWithoutMentions);
// Skip if no content to inject
if (!formattedContent) {
log('No content to inject after formatting');
return this.markAsExecuted(clonedContext);
// Inject agent-management context (providers/plugins) before the first user message
if (formattedContent) {
const firstUserIndex = clonedContext.messages.findIndex((msg) => msg.role === 'user');
if (firstUserIndex !== -1) {
const contextMessage = {
content: formattedContent,
createdAt: Date.now(),
id: `agent-management-context-${Date.now()}`,
meta: { injectType: 'agent-management-context', systemInjection: true },
role: 'user' as const,
updatedAt: Date.now(),
};
clonedContext.messages.splice(firstUserIndex, 0, contextMessage);
clonedContext.metadata.agentManagementContextInjected = true;
log('Agent Management context injected before first user message');
}
}
// Find the first user message index
const firstUserIndex = clonedContext.messages.findIndex((msg) => msg.role === 'user');
// Inject mentionedAgents delegation context AFTER the last user message
// This position makes the delegation instruction most salient to the model
if (hasMentionedAgents) {
const mentionedContent = formatMentionedAgentsContext(this.config.context.mentionedAgents!);
if (firstUserIndex === -1) {
log('No user messages found, skipping injection');
return this.markAsExecuted(clonedContext);
// Find the last user message index
let lastUserIndex = -1;
for (let i = clonedContext.messages.length - 1; i >= 0; i--) {
if (clonedContext.messages[i].role === 'user') {
lastUserIndex = i;
break;
}
}
if (lastUserIndex !== -1) {
const mentionMessage = {
content: mentionedContent,
createdAt: Date.now(),
id: `agent-mention-delegation-${Date.now()}`,
meta: { injectType: 'agent-mention-delegation', systemInjection: true },
role: 'user' as const,
updatedAt: Date.now(),
};
// Insert after the last user message
clonedContext.messages.splice(lastUserIndex + 1, 0, mentionMessage);
log('Mentioned agents delegation context injected after last user message');
}
}
// Insert a new user message with context before the first user message
const contextMessage = {
content: formattedContent,
createdAt: Date.now(),
id: `agent-management-context-${Date.now()}`,
meta: { injectType: 'agent-management-context', systemInjection: true },
role: 'user' as const,
updatedAt: Date.now(),
};
clonedContext.messages.splice(firstUserIndex, 0, contextMessage);
// Update metadata
clonedContext.metadata.agentManagementContextInjected = true;
log('Agent Management context injected as new user message');
return this.markAsExecuted(clonedContext);
}
}

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
evalContextInjected?: boolean;
}
}
const log = debug('context-engine:provider:EvalContextSystemInjector');
export interface EvalContext {

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
forceFinishInjected?: boolean;
}
}
const log = debug('context-engine:provider:ForceFinishSummaryInjector');
export interface ForceFinishSummaryInjectorConfig {

View file

@ -3,6 +3,13 @@ import debug from 'debug';
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
gtdPlanId?: string;
gtdPlanInjected?: boolean;
}
}
const log = debug('context-engine:provider:GTDPlanInjector');
/**

View file

@ -3,6 +3,15 @@ import debug from 'debug';
import { BaseLastUserContentProvider } from '../base/BaseLastUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
gtdTodoCompletedCount?: number;
gtdTodoCount?: number;
gtdTodoInjected?: boolean;
gtdTodoProcessingCount?: number;
}
}
const log = debug('context-engine:provider:GTDTodoInjector');
/** Status of a todo item */

View file

@ -1,21 +1,16 @@
import { escapeXml } from '@lobechat/prompts';
import debug from 'debug';
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
const log = debug('context-engine:provider:GroupAgentBuilderContextInjector');
declare module '../types' {
interface PipelineContextMetadataOverrides {
groupAgentBuilderContextInjected?: boolean;
}
}
/**
* Escape XML special characters
*/
const escapeXml = (str: string): string => {
return str
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
};
const log = debug('context-engine:provider:GroupAgentBuilderContextInjector');
/**
* Group member info for Group Agent Builder context

View file

@ -5,6 +5,12 @@ import debug from 'debug';
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
groupContextInjected?: boolean;
}
}
const log = debug('context-engine:provider:GroupContextInjector');
/**

View file

@ -3,6 +3,16 @@ import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
historySummary?: {
formattedLength: number;
injected: boolean;
originalLength: number;
};
}
}
const log = debug('context-engine:provider:HistorySummaryProvider');
/**

View file

@ -5,6 +5,14 @@ import debug from 'debug';
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
filesCount?: number;
knowledgeBasesCount?: number;
knowledgeInjected?: boolean;
}
}
const log = debug('context-engine:provider:KnowledgeInjector');
export interface KnowledgeInjectorConfig {

View file

@ -5,6 +5,12 @@ import debug from 'debug';
import { BaseLastUserContentProvider } from '../base/BaseLastUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
pageEditorContextInjected?: boolean;
}
}
const log = debug('context-engine:provider:PageEditorContextInjector');
export interface PageEditorContextInjectorConfig {

View file

@ -0,0 +1,92 @@
import { escapeXml } from '@lobechat/prompts';
import type { RuntimeSelectedSkill } from '@lobechat/types';
import debug from 'debug';
import { BaseLastUserContentProvider } from '../base/BaseLastUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
selectedSkillContext?: {
injected: boolean;
skillsCount: number;
};
}
}
const log = debug('context-engine:provider:SelectedSkillInjector');
export interface SelectedSkillInjectorConfig {
selectedSkills?: RuntimeSelectedSkill[];
}
const formatSelectedSkills = (selectedSkills: RuntimeSelectedSkill[]): string | null => {
if (selectedSkills.length === 0) return null;
const lines = [
'The user explicitly selected these skills for this request. Prefer them when relevant.',
'<selected_skills>',
...selectedSkills.map(
(skill) =>
` <skill identifier="${escapeXml(skill.identifier)}" name="${escapeXml(skill.name)}" />`,
),
'</selected_skills>',
];
return lines.join('\n');
};
/**
* Selected Skill Injector
* Appends user-selected slash-menu skills to the last user message as ephemeral context.
*/
export class SelectedSkillInjector extends BaseLastUserContentProvider {
readonly name = 'SelectedSkillInjector';
constructor(
private config: SelectedSkillInjectorConfig,
options: ProcessorOptions = {},
) {
super(options);
}
protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
const clonedContext = this.cloneContext(context);
const selectedSkills = this.config.selectedSkills ?? [];
if (selectedSkills.length === 0) {
log('No selected skills, skipping injection');
return this.markAsExecuted(clonedContext);
}
const content = formatSelectedSkills(selectedSkills);
if (!content) {
log('No selected skill content generated, skipping injection');
return this.markAsExecuted(clonedContext);
}
const lastUserIndex = this.findLastUserMessageIndex(clonedContext.messages);
if (lastUserIndex === -1) {
log('No user messages found, skipping injection');
return this.markAsExecuted(clonedContext);
}
const hasExistingWrapper = this.hasExistingSystemContext(clonedContext);
const contentToAppend = hasExistingWrapper
? this.createContextBlock(content, 'selected_skill_context')
: this.wrapWithSystemContext(content, 'selected_skill_context');
this.appendToLastUserMessage(clonedContext, contentToAppend);
clonedContext.metadata.selectedSkillContext = {
injected: true,
skillsCount: selectedSkills.length,
};
log('Selected skill context appended, skills count: %d', selectedSkills.length);
return this.markAsExecuted(clonedContext);
}
}

View file

@ -4,6 +4,15 @@ import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
skillContext?: {
injected: boolean;
skillsCount: number;
};
}
}
const log = debug('context-engine:provider:SkillContextProvider');
/**

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
systemDateInjected?: boolean;
}
}
const log = debug('context-engine:provider:SystemDateProvider');
export interface SystemDateProviderConfig {

View file

@ -3,6 +3,12 @@ import debug from 'debug';
import { BaseProvider } from '../base/BaseProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
systemRoleInjected?: boolean;
}
}
const log = debug('context-engine:provider:SystemRoleInjector');
export interface SystemRoleInjectorConfig {

View file

@ -4,6 +4,15 @@ import debug from 'debug';
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
toolDiscoveryContext?: {
injected: boolean;
toolsCount: number;
};
}
}
const log = debug('context-engine:provider:ToolDiscoveryProvider');
export interface ToolDiscoveryMeta {

View file

@ -7,6 +7,17 @@ import { ToolNameResolver } from '../engine/tools';
import type { LobeToolManifest } from '../engine/tools/types';
import type { PipelineContext, ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
toolSystemRole?: {
contentLength: number;
injected: boolean;
supportsFunctionCall: boolean;
toolsCount: number;
};
}
}
const log = debug('context-engine:provider:ToolSystemRoleProvider');
/**
@ -58,7 +69,7 @@ export class ToolSystemRoleProvider extends BaseProvider {
clonedContext.metadata.toolSystemRole = {
contentLength: toolSystemRole.length,
injected: true,
supportsFunctionCall: this.config.isCanUseFC(this.config.model, this.config.provider),
supportsFunctionCall: !!this.config.isCanUseFC(this.config.model, this.config.provider),
toolsCount: this.config.manifests.length,
};

View file

@ -5,6 +5,12 @@ import debug from 'debug';
import { BaseFirstUserContentProvider } from '../base/BaseFirstUserContentProvider';
import { type PipelineContext, type ProcessorOptions } from '../types';
declare module '../types' {
interface PipelineContextMetadataOverrides {
userMemoryInjected?: boolean;
}
}
const log = debug('context-engine:provider:UserMemoryInjector');
export interface UserMemoryInjectorConfig {

View file

@ -0,0 +1,205 @@
import { describe, expect, it } from 'vitest';
import type { PipelineContext } from '../../types';
import { AgentManagementContextInjector } from '../AgentManagementContextInjector';
describe('AgentManagementContextInjector', () => {
const createContext = (messages: any[]): PipelineContext => ({
initialState: { messages: [] },
isAborted: false,
messages,
metadata: {},
});
describe('disabled / no context', () => {
it('should skip when disabled', async () => {
const injector = new AgentManagementContextInjector({ enabled: false });
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'hi' },
]);
const result = await injector.process(ctx);
expect(result.messages).toHaveLength(2);
});
it('should skip when no context provided', async () => {
const injector = new AgentManagementContextInjector({ enabled: true });
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'hi' },
]);
const result = await injector.process(ctx);
expect(result.messages).toHaveLength(2);
});
});
describe('agent-management context (providers/plugins)', () => {
it('should inject before the first user message', async () => {
const injector = new AgentManagementContextInjector({
enabled: true,
context: {
availableProviders: [
{
id: 'openai',
name: 'OpenAI',
models: [{ id: 'gpt-4', name: 'GPT-4' }],
},
],
},
});
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'create an agent' },
]);
const result = await injector.process(ctx);
expect(result.messages).toHaveLength(3);
expect(result.messages[0].role).toBe('system');
// Injected context before user message
expect(result.messages[1].role).toBe('user');
expect(result.messages[1].content).toContain('<agent_management_context>');
expect(result.messages[1].content).toContain('gpt-4');
// Original user message
expect(result.messages[2].content).toBe('create an agent');
expect(result.metadata.agentManagementContextInjected).toBe(true);
});
});
describe('mentionedAgents delegation', () => {
it('should inject delegation context after the last user message', async () => {
const injector = new AgentManagementContextInjector({
enabled: true,
context: {
mentionedAgents: [{ id: 'agt_designer', name: 'Designer Agent' }],
},
});
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'Let @Designer Agent help me' },
]);
const result = await injector.process(ctx);
// system + user + injected delegation
expect(result.messages).toHaveLength(3);
expect(result.messages[1].content).toBe('Let @Designer Agent help me');
const delegationMsg = result.messages[2];
expect(delegationMsg.role).toBe('user');
expect(delegationMsg.content).toContain('<mentioned_agents>');
expect(delegationMsg.content).toContain('agt_designer');
expect(delegationMsg.content).toContain('Designer Agent');
expect(delegationMsg.content).toContain('MUST use the callAgent tool');
expect(delegationMsg.meta.injectType).toBe('agent-mention-delegation');
});
it('should inject after the LAST user message, not the first', async () => {
const injector = new AgentManagementContextInjector({
enabled: true,
context: {
mentionedAgents: [{ id: 'agt_1', name: 'Agent A' }],
},
});
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'first message' },
{ role: 'assistant', content: 'reply' },
{ role: 'user', content: 'Let @Agent A do this' },
]);
const result = await injector.process(ctx);
// system + user + assistant + user + injected
expect(result.messages).toHaveLength(5);
expect(result.messages[3].content).toBe('Let @Agent A do this');
expect(result.messages[4].content).toContain('<mentioned_agents>');
expect(result.messages[4].content).toContain('agt_1');
});
it('should handle multiple mentioned agents', async () => {
const injector = new AgentManagementContextInjector({
enabled: true,
context: {
mentionedAgents: [
{ id: 'agt_1', name: 'Agent A' },
{ id: 'agt_2', name: 'Agent B' },
],
},
});
const ctx = createContext([{ role: 'user', content: 'hello' }]);
const result = await injector.process(ctx);
const delegationMsg = result.messages[1];
expect(delegationMsg.content).toContain('agt_1');
expect(delegationMsg.content).toContain('agt_2');
expect(delegationMsg.content).toContain('Agent A');
expect(delegationMsg.content).toContain('Agent B');
});
});
describe('combined: agent-management + mentionedAgents', () => {
it('should inject providers before first user and delegation after last user', async () => {
const injector = new AgentManagementContextInjector({
enabled: true,
context: {
availableProviders: [
{
id: 'anthropic',
name: 'Anthropic',
models: [{ id: 'claude-sonnet-4-5-20250514', name: 'Claude Sonnet' }],
},
],
mentionedAgents: [{ id: 'agt_dev', name: 'Developer' }],
},
});
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'Let @Developer build this' },
]);
const result = await injector.process(ctx);
// system + management_context + user + delegation
expect(result.messages).toHaveLength(4);
// Management context before first user
expect(result.messages[1].content).toContain('<agent_management_context>');
expect(result.messages[1].content).toContain('claude-sonnet-4-5-20250514');
// Management context should NOT contain mentionedAgents
expect(result.messages[1].content).not.toContain('<mentioned_agents>');
// Original user message
expect(result.messages[2].content).toBe('Let @Developer build this');
// Delegation after last user
expect(result.messages[3].content).toContain('<mentioned_agents>');
expect(result.messages[3].content).toContain('agt_dev');
});
});
describe('only mentionedAgents (no providers/plugins)', () => {
it('should NOT inject empty agent-management context but SHOULD inject delegation', async () => {
const injector = new AgentManagementContextInjector({
enabled: true,
context: {
// No providers, no plugins — only mentionedAgents
mentionedAgents: [{ id: 'agt_x', name: 'Agent X' }],
},
});
const ctx = createContext([
{ role: 'system', content: 'sys' },
{ role: 'user', content: 'Ask @Agent X' },
]);
const result = await injector.process(ctx);
// system + user + delegation (no empty management context)
expect(result.messages).toHaveLength(3);
expect(result.messages[1].content).toBe('Ask @Agent X');
expect(result.messages[2].content).toContain('<mentioned_agents>');
expect(result.messages[2].content).toContain('agt_x');
});
});
});

View file

@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest';
import type { PipelineContext } from '../../types';
import { SelectedSkillInjector } from '../SelectedSkillInjector';
const createContext = (messages: any[] = []): PipelineContext => ({
initialState: {
messages: [],
model: 'gpt-4',
provider: 'openai',
},
isAborted: false,
messages,
metadata: {
maxTokens: 4096,
model: 'gpt-4',
},
});
describe('SelectedSkillInjector', () => {
it('should append selected skills to the last user message', async () => {
const provider = new SelectedSkillInjector({
selectedSkills: [
{ identifier: 'user_memory', name: 'User Memory' },
{ identifier: 'instruction', name: 'Instruction' },
],
});
const context = createContext([
{ content: 'Earlier question', id: 'user-1', role: 'user' },
{ content: 'Assistant reply', id: 'assistant-1', role: 'assistant' },
{ content: 'Current request', id: 'user-2', role: 'user' },
]);
const result = await provider.process(context);
expect(result.messages).toHaveLength(3);
expect(result.messages[2].content).toContain('Current request');
expect(result.messages[2].content).toContain('<selected_skill_context>');
expect(result.messages[2].content).toContain('<selected_skills>');
expect(result.messages[2].content).toContain(
'<skill identifier="user_memory" name="User Memory" />',
);
expect(result.metadata.selectedSkillContext).toEqual({
injected: true,
skillsCount: 2,
});
});
it('should reuse existing system context wrapper on the last user message', async () => {
const provider = new SelectedSkillInjector({
selectedSkills: [{ identifier: 'user_memory', name: 'User Memory' }],
});
const context = createContext([
{
content: `Current request
<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
<current_page_context>
<page>draft</page>
</current_page_context>
<!-- END SYSTEM CONTEXT -->`,
id: 'user-1',
role: 'user',
},
]);
const result = await provider.process(context);
const content = result.messages[0].content as string;
expect(content.match(/<!-- SYSTEM CONTEXT \(NOT PART OF USER QUERY\) -->/g)).toHaveLength(1);
expect(content).toContain('<current_page_context>');
expect(content).toContain('<selected_skill_context>');
});
});

View file

@ -44,8 +44,8 @@ describe('ToolSystemRoleProvider', () => {
// Should update metadata
expect(result.metadata.toolSystemRole).toBeDefined();
expect(result.metadata.toolSystemRole.injected).toBe(true);
expect(result.metadata.toolSystemRole.supportsFunctionCall).toBe(true);
expect(result.metadata.toolSystemRole!.injected).toBe(true);
expect(result.metadata.toolSystemRole!.supportsFunctionCall).toBe(true);
});
it('should merge tool system role with existing system message', async () => {

View file

@ -12,6 +12,7 @@ export { HistorySummaryProvider } from './HistorySummary';
export { KnowledgeInjector } from './KnowledgeInjector';
export { PageEditorContextInjector } from './PageEditorContextInjector';
export { PageSelectionsInjector } from './PageSelectionsInjector';
export { SelectedSkillInjector } from './SelectedSkillInjector';
export { SkillContextProvider } from './SkillContextProvider';
export { SystemDateProvider } from './SystemDateProvider';
export { SystemRoleInjector } from './SystemRoleInjector';
@ -51,6 +52,7 @@ export type { HistorySummaryConfig } from './HistorySummary';
export type { KnowledgeInjectorConfig } from './KnowledgeInjector';
export type { PageEditorContextInjectorConfig } from './PageEditorContextInjector';
export type { PageSelectionsInjectorConfig } from './PageSelectionsInjector';
export type { SelectedSkillInjectorConfig } from './SelectedSkillInjector';
export type { SkillContextProviderConfig, SkillMeta } from './SkillContextProvider';
export type { SystemDateProviderConfig } from './SystemDateProvider';
export type { SystemRoleInjectorConfig } from './SystemRoleInjector';

View file

@ -1,5 +1,17 @@
import type { UIChatMessage } from '@lobechat/types';
/**
* Consumer-side metadata extensions for PipelineContext.metadata.
*
* Example:
* declare module '@lobechat/context-engine' {
* interface PipelineContextMetadataOverrides {
* myCustomFlag?: boolean;
* }
* }
*/
export interface PipelineContextMetadataOverrides {}
/**
* Agent state - inferred from original project types
*/
@ -39,6 +51,19 @@ export interface Message {
role: string;
}
/**
* Metadata shared across pipeline processors.
* Consumers can extend this through declaration merging on
* `LobeChatContextEngine.PipelineContextMetadataOverrides`.
*/
export interface PipelineContextMetadata extends PipelineContextMetadataOverrides {
[key: `${string}InjectedCount`]: number | undefined;
currentTokenCount?: number;
maxTokens?: number;
model?: string;
provider?: string;
}
/**
* Pipeline context - core data structure flowing through the pipeline
*/
@ -55,16 +80,7 @@ export interface PipelineContext {
/** Mutable message list being built */
messages: Message[];
/** Metadata for communication between processors */
metadata: {
/** Other custom metadata */
[key: string]: any;
/** Current token count estimate */
currentTokenCount?: number;
/** Maximum token limit */
maxTokens?: number;
/** Model identifier */
model?: string;
};
metadata: PipelineContextMetadata;
}
/**
@ -98,7 +114,7 @@ export interface PipelineResult {
/** Final processed messages */
messages: any[];
/** Metadata from processing */
metadata: Record<string, any>;
metadata: PipelineContextMetadata;
/** Execution statistics */
stats: {
/** Number of processors processed */

View file

@ -217,6 +217,7 @@ export class MessageModel {
id: messages.id,
role: messages.role,
content: messages.content,
editorData: messages.editorData,
reasoning: messages.reasoning,
search: messages.search,
metadata: messages.metadata,
@ -555,6 +556,7 @@ export class MessageModel {
id: messages.id,
role: messages.role,
content: messages.content,
editorData: messages.editorData,
reasoning: messages.reasoning,
search: messages.search,
metadata: messages.metadata,

View file

@ -1,2 +1,3 @@
export * from './crawlResults';
export * from './searchResults';
export * from './xmlEscape';

View file

@ -1,3 +1,17 @@
/**
* Escape all XML special characters (safe for both attributes and content)
* Includes: & < > " '
*/
export const escapeXml = (text: string | undefined | null): string => {
if (!text) return '';
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
};
/**
* Escape special characters for XML attributes
* Includes: & " < >

View file

@ -1,6 +1,9 @@
import { z } from 'zod';
import type { MessageMetadata } from './message/common';
import { ChatToolPayloadSchema, MessageMetadataSchema } from './message/common';
import type { UIChatMessage } from './message';
import type { CreateMessageParams } from './message/ui/params';
import type { PageSelection } from './message/ui/params';
import { PageSelectionSchema } from './message/ui/params';
import type { OpenAIChatMessage } from './openai/chat';
@ -12,6 +15,8 @@ import { ThreadType } from './topic/thread';
export interface SendNewMessage {
content: string;
/** Lexical editor JSON state for rich text rendering */
editorData?: Record<string, any>;
// if message has attached with files, then add files to message and the agent
files?: string[];
/** Page selections attached to this message (for Ask AI functionality) */
@ -19,6 +24,15 @@ export interface SendNewMessage {
parentId?: string;
}
export interface SendPreloadMessage
extends Omit<
Pick<CreateMessageParams, 'content' | 'metadata' | 'plugin' | 'tool_call_id' | 'tools'>,
'metadata'
> {
metadata?: MessageMetadata;
role: 'assistant' | 'tool';
}
/**
* Parameters for creating a new thread along with message
*/
@ -57,6 +71,7 @@ export interface SendMessageServerParams {
title?: string;
topicMessageIds?: string[];
};
preloadMessages?: SendPreloadMessage[];
newUserMessage: SendNewMessage;
sessionId?: string;
threadId?: string;
@ -71,6 +86,22 @@ export const CreateThreadWithMessageSchema = z.object({
type: z.enum([ThreadType.Continuation, ThreadType.Standalone, ThreadType.Isolation]),
});
const SendPreloadMessageSchema = z.object({
content: z.string(),
metadata: MessageMetadataSchema.optional(),
plugin: z
.object({
apiName: z.string(),
arguments: z.string(),
identifier: z.string(),
type: z.string(),
})
.optional(),
role: z.enum(['assistant', 'tool']),
tool_call_id: z.string().optional(),
tools: z.array(ChatToolPayloadSchema).optional(),
});
export const AiSendMessageServerSchema = z.object({
agentId: z.string().optional(),
groupId: z.string().optional(),
@ -86,8 +117,10 @@ export const AiSendMessageServerSchema = z.object({
topicMessageIds: z.array(z.string()).optional(),
})
.optional(),
preloadMessages: z.array(SendPreloadMessageSchema).optional(),
newUserMessage: z.object({
content: z.string(),
editorData: z.record(z.unknown()).optional(),
files: z.array(z.string()).optional(),
pageSelections: z.array(PageSelectionSchema).optional(),
parentId: z.string().optional(),

View file

@ -91,6 +91,7 @@ export interface NewMessage {
export interface UpdateMessageParams {
content?: string;
editorData?: Record<string, any> | null;
error?: ChatMessageError | null;
imageList?: ChatImageItem[];
metadata?: MessageMetadata;
@ -117,6 +118,7 @@ export interface NewMessageQueryParams {
export const UpdateMessageParamsSchema = z
.object({
content: z.string().optional(),
editorData: z.record(z.any()).nullable().optional(),
error: ChatMessageErrorSchema.nullable().optional(),
imageList: z.array(ChatImageItemSchema).optional(),
metadata: MessageMetadataSchema.optional(),

View file

@ -113,6 +113,8 @@ export interface UIChatMessage {
compressedMessages?: UIChatMessage[];
content: string;
createdAt: number;
/** Lexical editor JSON state for rich text rendering */
editorData?: Record<string, any> | null;
error?: ChatMessageError | null;
// Extended fields
extra?: ChatMessageExtra;

Some files were not shown because too many files have changed in this diff Show more