mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
fix/topic-switch-message-refresh
190 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
abdd7ff74f |
♻️ refactor(context-engine): let the engine assemble the system message (#19485)
* ♻️ refactor(context-engine): let the engine assemble the system message The server composed the final system prompt by string-appending onto `agentConfig.systemRole` from three files across the pipeline: the resolver, `operationPrep` (project instructions), and `toolDiscovery` (borrowed-connector attribution). That is why the resolved config had to stay mutable all the way through, and why each append site carried a comment vouching for the aliasing — "appending here still reaches the LLM because `createOperation` reads `agentConfig` below". Correctness depended on nobody cloning the object, which the `do not clone` warning enforced by convention rather than by type. The mechanism for doing this properly already existed and was being bypassed. `BaseSystemRoleProvider` is purpose-built — subclasses return content, the base finds or creates the system message and joins with `\n\n` — and the engine has a declared "Phase 2: System Message Assembly" where a dozen injectors already use it. So the two late contributions become injectors. `ProjectInstructionsInjector` takes the files rather than a rendered block, because wrapping them is presentation and belongs to whoever owns the system message. `ConnectorOwnershipInjector` takes a rendered note, because naming the authorizers needs a user lookup the engine cannot perform — the same split `EvalContextSystemInjector` makes with `envPrompt`. Both run immediately after `SystemRoleInjector`, which is exactly where their text used to land. Placing them later would silently reorder the prompt behind every other Phase 2 provider; keeping the position keeps it byte-identical. The data reaches the engine on `AgentConfigWithId`, which already carries run-scoped fields stamped by one stage for another (`subAgentChatConfigOverride`). Same transport as before, structured payload instead of a concatenated string — no new plumbing through the state blob, and no size change, since the same text already travelled inside `systemRole`. Not included: the persona-level appends inside the resolver (builtin runtime role, page/task agent, per-call instructions). Those compose what the agent IS, which the resolve phase owns; these two are run context, which the engine owns. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(agent): carry system-message context on the operation, not the config Follow-up to the previous commit, which moved the two late system-prompt contributions into context-engine injectors but carried their payloads on `AgentConfigWithId`. That was the same category error in a better shape: it removed the string concatenation and kept smuggling run context through the agent's configuration. Project instructions come from the workspace and the connector note from tool discovery — neither is part of what the agent IS. They now travel the way every other piece of run context does. There is an established family of them on the operation metadata — `agentShareVisitor`, `botContext`, `botPlatformContext`, `discordContext`, `userTimezone`, `evalContext` — each mapped onto `RuntimeExecutorContext` and consumed downstream, and `evalContext` in particular already feeds an injector. `expertise` is the exact precedent and this follows it end to end: `prepareOperation` and `discoverTools` return the values instead of mutating the config, `startOperation` puts them on the operation, `createOperation` persists them into `state.metadata`, `AgentRuntimeService` maps them onto the executor context, and the context builder reads them from there. Persisting matters rather than being incidental: steps can be claimed by different workers, so anything the engine needs has to survive on the operation. `createOperation` builds `state.metadata` from an explicit field list, so a value not named there is dropped without a word — the same shape of trap as the zod schema that silently stripped `createdAt`. The two fields added to `AgentConfigWithId` in the previous commit are removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): forward the system-message context, and restore its order Two from review, and the first one meant the whole change was dead on the server. `serverMessagesEngine` destructures a fixed parameter list, so the two new fields never reached `MessagesEngine` and both injectors always saw `undefined`. The old concatenation was already gone, so every server run would have lost its project instructions and connector attribution outright — `AGENTS.md` silently absent from the prompt. Neither the injector unit tests nor the type checker caught it: the tests exercised the injectors directly, and the call site passes an intermediate object, which disables excess-property checking. So the regression test lives at the wrapper-to-engine seam, not on the injectors. Verified it fails without the forwarding. The injector order was also wrong. `discoverTools` runs before `prepareOperation`, so the old appends produced persona → connector note → project instructions; the injectors had them the other way round, against an adjacent comment claiming the prompt was byte-identical. Connector attribution now precedes the project instructions, with a test pinning it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(agent): cover every link the system-message context travels The forwarding bug shipped with green injector tests because they exercised the injectors directly. The wrapper test added with the fix only covers the last link; a value dropped anywhere earlier is just as silent and just as total. Every failure this feature has had — the wrapper's fixed parameter list, and `createOperation` assembling `state.metadata` from an explicit field list — was a name missing from a list rather than broken logic. Nothing about that shape surfaces in behaviour: the injectors keep working, they simply never receive anything. So each link now has an assertion of its own, and each was verified to fail with its forwarding removed: createOperation params → state.metadata (AgentRuntimeService.test) executor context → engine input (serverCallLlmContextBuilder.test) engine input → system message (serverMessagesEngine.test) One link is still covered by reading rather than by a test: `state.metadata` → executor context. And a true end-to-end run through `execAgent` is disproportionately expensive here, because the project instructions originate in the device workspace-scan path — reaching them needs an activeDeviceId, a device row and a scan result, none of which is the thing under test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(agent): pin the stage order the system message depends on Drives the real `execAgent` pipeline and asserts that tool discovery runs before operation prep. That order is the reason connector attribution precedes the project instructions in the assembled system message, and until now it rested on a comment citing two line numbers — the same kind of claim that was wrong the first time this ordering was written down. Carrying a real payload end to end through this harness turned out to cost more than it returns. The project instructions originate in the device workspace-scan path, so reaching them needs an active device, a device row, a fresh cached scan, and the agent-skill lookup that happens to share the same `try` — each mock revealing the next, and none of it the thing under test. The payload is therefore covered link by link instead, which also matches the failure shape: every regression it has had was a name missing from an explicit field list, not broken logic. Worth recording separately: project instructions and the project skill lookup share one `try`, so a skill-model failure silently drops the instructions too. Pre-existing, unrelated to this change, and not fixed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6b85e332f1 |
🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile (#19462)
* 🐛 fix(desktop): pin bundled npm deps through the Desktop lockfile The main hash accepted any npm module that happened to resolve inside apps/desktop/node_modules, so a module linked from outside the frozen install (or from a stale store entry) could enter the renderer OTA hash without the lockfile ever pinning it. Resolve each bundled module to its package name@version and require apps/desktop/pnpm-lock.yaml to pin it, keeping the file:/link:/git/tarball escape hatch for specifiers that pin by resolution instead of version. partial-json is imported by packages/utils and packages/context-engine but was never declared, so the Desktop lockfile could not pin it: declare it in both packages and record it in apps/desktop/pnpm-lock.yaml. * 🔊 chore(desktop): name the changed bundles before a dev restart The dev orchestrator logged only that a main/preload bundle changed, which cannot distinguish the debounced restart from an unrelated vite rebuild. Collect every file reported by the watcher during the debounce window and print it with its event type, size and mtime. * 📝 docs(acceptance): publish the round from the session's own observations A delivery verified on the real product earlier in the session still had to re-run the plan and checker stage to obtain a published round. Document that those observations and artifacts are the evidence: write plan[] and cases[] from them, disclose anything never captured, and ingest. |
||
|
|
18131e9f75 |
🐛 fix(agent-runtime): recover garbled tool names instead of ending the turn (#19362)
* 🐛 fix(agent-runtime): recover garbled tool names instead of ending the turn When a model garbles the `____` separator in a tool name, `ToolNameResolver` dropped the call, `GeneralChatAgent` saw no tool calls and no text, and the operation finished as `done` with an empty assistant message — the conversation just stops mid-task with no error anywhere. Observed on tpc_t1n5DfJ0yqtv: glm-5.3-flash emitted `lobe-local-system~~__runCommand` at step 4 after spelling `lobe-local-system____runCommand` correctly at steps 0 and 2. - `ToolNameResolver.resolve` now retries an unresolved call against a squashed form of the offered tool names (lowercase, alphanumeric only). Only an unambiguous match is accepted, and it runs strictly after the existing resolution path, so nothing that resolves today changes. - When a call still cannot be resolved, `GeneralChatAgent` answers it with a rejected tool result (`tool_name_unresolved`) so the model can retry with a real name, instead of finishing silently. The feedback is capped at two rounds per operation, is skipped once `forceFinish` is set, and falls back to throwing so the failure lands on the dashboards as an error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent-runtime): make the unresolved-tool rejection actually reach the model Two problems found in review of the previous commit. The rejected tool rows were written under a parent assistant that carries no `tools`, so they never came back: state messages are rebuilt from the DB on every step, and conversation-flow's `collectToolMessages` returns early when the parent lists no calls. The feedback the model needed was dropped on the way in. `resolve_blocked_tools` now advertises those calls on the parent before creating the rows, behind an explicit `unresolvedToolNames` payload flag so the ordinary allow-list path keeps its single write. The feedback budget counted matching tool rows in `state.messages`, which is rehydrated history: rejections from earlier operations spent a new operation's budget before it started, and two malformed parallel calls consumed both rounds at once. It is now an operation-scoped counter on `AgentState`, incremented once per instruction by the executor. Also names the offending tools in the rejection text. The name the model reads back from its own turn is regenerated from the persisted identifier/apiName pair, which for an unparseable name is not what it emitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent-runtime): keep the thought signature on unresolvable tool calls A garbled tool name does not imply a garbled signature. Gemini 3.x attaches `thoughtSignature` to the call and 400s on the next tool-call turn unless it comes back, so rebuilding the payload without it would break exactly the retry the rejection exists to enable. Carried over like the normal resolver does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dbc1e47c1b |
✨ feat(skill-store): route skill links to the Skill Store instead of the crawler (#19331)
* 🐛 fix(skill-store): route LobeHub skill URLs to importFromMarket instead of the crawler A `lobehub.com/skills/{identifier}/skill.md` link in a user message was reaching the model with no turn-local guidance. The only rule covering the case lived in the activator's system role, far from the current turn — and the page, once crawled, carries its own "always use the CLI, do NOT make raw API requests" instructions plus a full `npx @lobehub/market-cli register && skills install` walkthrough. Fresher and more specific won: the model crawled the page and went to the sandbox, when the identifier it needed was already in the URL path. - Add `SkillImportRouteInjector`: extracts the marketplace identifier from any LobeHub skill URL in the current user message and appends an explicit instruction to import it via `lobe-skill-store`, stating that it takes precedence over install steps found on the linked page. Gated on the Skill Store being enabled or listed as available. - `SkillStoreManifest.meta.description` now carries the URL → `importFromMarket` rule. Before activation this single line is the only thing the model sees about the tool — its `systemRole` is not injected yet — so a keyword list alone left it outweighed by every already-active tool. - Skill Store `systemRole`: install from the URL you were given rather than browsing to it first; never run marketplace CLI commands; never conclude a skill is missing from an empty `searchSkill` (matching is textual, so "pptx presentation slides" misses a skill named "pptx"); keep queries short. - Activator `systemRole`: generic-tool fallback now requires that no skill URL or identifier was given, instead of firing whenever a search came back empty; ban marketplace CLI commands; `<available_tools>` is described as injected system context rather than a section of the system prompt, which is where it actually lands (`ToolDiscoveryProvider` extends `BaseFirstUserContentProvider`). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE * ♻️ refactor(skill-store): make the CLI a ranked last resort, not a flat ban The first pass banned the marketplace CLI outright. That overshoots: the CLI is the legitimate path for an agent that has no Skill Store tool, and an absolute "never" would strand it. The real defect was ordering, not the CLI itself. Replace the prohibition with an explicit ladder — importFromMarket → importSkill → CLI — stated in all four places the model can read it (the turn-local injection, the pre-activation manifest description, and both system roles), with the reason the CLI sits last: it needs a device registration the tools don't, is rate-limited 5 per 30 minutes per IP, and needs a working sandbox. Falling through to it now requires having tried the steps above and saying what failed. Also keeps the rebuttal to the phrase that caused the original detour — "install it as documented" does not promote the CLI, because a skill page documents the CLI for agents that lack this tool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE * ✨ feat(skill-store): route any skill source to importSkill, not just marketplace URLs The route only recognised `lobehub.com/skills/{identifier}`, which left out most of what `importSkill` actually accepts — a SKILL.md on any host, a GitHub repo or subdirectory, a ZIP package. Those URLs fell straight back to the crawler, which is the failure this route exists to prevent. Classify every URL in the message instead of matching one host: - `lobehub.com/skills/{identifier}` → `importFromMarket` with the identifier - a `SKILL.md` on any host, or a GitHub path with a `skills/` segment → `importSkill` type `url`; both are unambiguous, so no further signal is needed - an ordinary GitHub repo or a `.zip` → `importSkill`, but only when the message also expresses install intent (en + zh). A repo link in a message about reviewing code must not be read as "install this skill" Each detected entry now carries the resolved call, so the injected block names `importFromMarket` with its identifier or `importSkill` with its url and type rather than describing one shape. `metadata.skillImportRoute` reports urls, since an identifier no longer exists for every route. `lobehub.com/skills/skill.md` has no identifier to extract and now falls through to `importSkill` rather than being dropped — it is still a SKILL.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE * 🔒 fix(skill-store): drop trailing URL punctuation without a quadratic regex CodeQL flagged `/[!,.:;?、。,:;?]+$/` as polynomial ReDoS on uncontrolled data, and it is reachable. The pattern has an end anchor but no start anchor, so the engine retries from every offset; on a URL ending in a long punctuation run followed by one more character it does O(n^2) work. `!` and `.` are both inside `URL_PATTERN`'s character class, so `https://example.com/` + 200k `!` + `a` survives URL matching as a single token and reaches the replace — straight from message text. Replace it with a backward scan over a Set. Same result, linear, no backtracking. The regression test uses that 200k input and asserts it completes well inside a second; the previous implementation would not have finished. Reviewed the file's other patterns while here — all either start-anchored against a single URL or a linear scan, and the one nested quantifier (`(?:[\w-]+\.)*` in the marketplace pattern) has a forced split because `[\w-]` cannot match the `.` that terminates each repetition. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZvgLyect6csrBufui73ZE --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d3c25f2a95 |
🐛 fix(bot): read Feishu chat history and decode WeChat voice (#19323)
* 🐛 fix(bot): read Feishu chat history, decode WeChat voice and gate personal-file visibility toggles - Feishu/Lark (LOBE-13803): inject the current `{ platform, channelId }` into the bot platform prompt so `readMessages` no longer asks the user for a group ID; explain `230027` and friends with the exact scope to add (`im:message.group_msg`); surface Lark `permission_violations` in API errors; map unicode reactions to Feishu `emoji_type` and track `reaction_id` so step swaps replace instead of stacking; decide the no-@mention exemption from real chat membership (`user_count`/`bot_count`, fail closed) instead of who has spoken. - WeChat (LOBE-13407): decode SILK voice to WAV on download via `silk-wasm` so the attachment is playable in the browser and usable by audio models. - ResourceManager: only offer "Make private" / "Publish to workspace" for the creator's own files inside a workspace; personal-mode rows default to `visibility = 'public'` and were wrongly offered the toggle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ * 🐛 fix(feishu): read the newest history page and bound solo-chat cache staleness Address the Codex review on #19323: - `readMessages` and the chat-SDK `fetchMessages` asked Feishu for history without `sort_type`, which defaults to ascending — in any chat past the page cap the model got the chat's oldest messages instead of what was just said. Request `ByCreateTimeDesc` (kept on every cursor page) and flip the page so the documented chronological order still holds; `forward` maps to ascending. - A cached "solo" verdict kept the bot answering every unmentioned message for 10 minutes after someone joined the chat. Positive verdicts now expire after 60 s; only "shared" verdicts keep the 10-minute TTL. - Without agent-runtime Redis the verdict was never cached, costing a blocking Feishu request per inbound message. Fall back to a bounded process-local cache with the same TTLs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ * 🐛 fix(bot): skip membership lookups for addressed messages and keep the reaction id until deletion succeeds Second Codex round on #19323: - `onSubscribedMessage` only consults `isSoloBotConversation` when a DM or @mention has not already settled the question, so a lapsed cache never puts a platform round-trip in front of a direct message. - The Feishu final reaction clear forgot the tracked `reaction_id` before the remote delete ran; a transient failure left the reaction visible with nothing to retry with. Delete the tracker entry only after removal succeeds. - Fix the `result.messages` possibly-undefined type error CI caught in `service.test.ts`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ * 🐛 fix(bot): keep every placed Feishu reaction id until removal succeeds and scope Slack history injection Third Codex round on #19323: - Feishu reaction tracker keeps a list of ids per message, in Redis or a bounded process-local cache when agent-runtime Redis is absent (local runs used to stack every step reaction). A swap that adds the next reaction but fails to remove the previous one keeps both ids; the final clear retries removal in place before handing leftovers back to the tracker, since neither the bridge nor the queue callback retries it. Legacy bare-id keys still read. - `chatComposition` shares the same bounded memory cache helper. - New optional `PlatformClient.extractConversationId`: Slack declines for reply threads, so the injected current conversation never points `readMessages` (channel history only) at the parent channel of a thread. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V5Mi6LJCFiMnej3MPAjMpJ --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
45e923e302 |
✨ feat(workspace): mirror account settings, inject workspace context, keep acceptance in scope (#19299)
* ✨ feat(workspace): mirror account settings, inject workspace context and keep acceptance in scope - LOBE-13896: mirror the account-level settings tabs (profile / appearance / hotkey / messenger / advanced / labs / about) under `/:workspaceSlug/settings` and regroup both settings sidebars (Account → Personal/Workspace → … → System → Developer) - LOBE-13895: add `WorkspaceContextInjector` so the system prompt carries the app origin and the workspace name/slug, with slug-prefixed link rules, on both the server runtime and the client context pipeline - LOBE-13898: open a task's acceptance in the side panel instead of navigating to the workspace-less `/acceptance/:id` page, which dropped the slug and flipped the app back to personal scope Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy * 🐛 fix(workspace): keep the workspace name out of the prompt and register the mobile hotkey route - only the validated slug is injected into `<workspace_context>`; the admin-controlled display name never reaches the system prompt - mirror `/:workspaceSlug/settings/hotkey` in the mobile router so the allowlisted path resolves there too Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy * 🐛 fix(prompts): trim trailing slashes without a polynomial regex CodeQL flagged `/\/+$/` on the app origin as polynomial on long runs of '/'; a character loop is linear and equivalent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy * 🐛 fix(prompts): scope the workspace link rules to in-app links The link rules forbade lobehub.com outright, which also stopped the model from linking the LobeHub homepage in ordinary chats. They now apply only to self-composed links into the app. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy * ✅ test(task): cover the acceptance-row click with a hook regression test Extracts the panel-opening behaviour into `useOpenAcceptanceInPanel` so the LOBE-13898 fix is asserted in a hook test: it toggles the task panel and opens the acceptance, and never routes to the workspace-less `/acceptance/:id` page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy * ✅ test: repair the two canary-red suites so Test CI can pass - `ApiKey.test.tsx`: `@lobehub/ui@5.40.2` (published 2026-09-08, bumps `@base-ui/react` to 1.8.0) no longer toggles the dialog checkbox on a synthetic Space key or a bare click on the `role=checkbox` span; click the wrapping label instead, which is what a real pointer hits (LOBE-13941) - `localization.test.ts`: drop the `content` field that #19293 removed from `BuiltinSkillManifest` (LOBE-13942) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YBAZ3KE8AknkvY1ZDKJJy --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
a5e74a9eb4 |
⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path (#19294)
* ⚡️ perf(spa): cut the editor runtime, ajv and database models from the boot path Four package-level import edges put ~1.5 MB raw on the first screen: - GlobalProvider/Editor.tsx imported @lobehub/editor/react, and rolldown does not shake that entry down to EditorProvider, so lexical, yjs and fuse landed in the entry chunk. The provider is reached by path until the package exposes a provider-only entry. - @lobechat/types re-exported the ajv-backed AgentGraphSchema through the barrel that every store touches; it now lives on the @lobechat/types/agent/graph subpath and the agent barrel re-exports types only. - @lobechat/context-engine's root barrel dragged the database models barrel (drizzle) in through AgentDocumentInjector; the app reaches the ./agent-document subpath and the injector reads loadPolicy directly. - packages/locales bundled locales/en-US/*.json on top of the default sources it mirrors; the overlay now applies only in development. Web production build: first-screen gzip 2299 KB -> 1968 KB. Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH * 🐛 fix(vite): keep one EditorContext in dev for the path-imported provider Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH * 🐛 fix(locales): refresh consumers after the dev English overlay lands Claude-Session: https://claude.ai/code/session_01QKr8CsFLYmrgqbRTSipTNH |
||
|
|
4a935c75eb |
✨ feat(chat): inject the agent's name and title into the system context (#19214)
* ✨ feat(chat): inject the agent's name and title into the system context An agent's identity lives on the agent row (`name` / `title`), not in its prompt text — so an agent with a user-given name but no self-introduction in its systemRole answered "who are you?" with the product or model name instead of the name the user gave it. Add an `<agent_identity>` block (new AgentIdentityInjector, appended right after the system role) carrying the personal name and role title, wired through both pipelines: the client contextEngineering (from agent meta) and the server runtime context builder (from the agent config row). Group chat is excluded — GroupContextInjector already establishes identity there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(agent): let the renamed default assistant introduce itself by its own name The inbox system role hardcoded "You are Lobe" — renaming the default assistant changed every surface except the model's own self-concept, so it kept answering "who are you?" as Lobe. Parametrize the inbox system role with the agent's name/title (RuntimeContext gains agentName/agentTitle), wired from the agent row at both runtime-config call sites (client agentConfigResolver + server resolveRunAgentConfig). Falls back to "Lobe" when the assistant was never renamed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5590527309 |
✨ feat(file): support .v / .sv (Verilog / SystemVerilog) attachments (#19118)
* ✨ feat(file): support .v / .sv (Verilog / SystemVerilog) attachments Wire HDL source files through the full attachment chain so .v/.sv files upload, parse, and reach the model like other text code attachments: - uploadGuard: add `v` / `sv` to the chat document-extension whitelist so plain-chat uploads are no longer rejected - utils/mimeType: map `.v` → `text/x-verilog`, `.sv` → `text/x-systemverilog` in CUSTOM_MIME_TYPES (mime-db has no entries for either, so they used to resolve to `application/octet-stream`) - file-loaders: add `v` / `sv` to TEXT_READABLE_FILE_TYPES so the server DocumentService.parseFile dispatch uses TextLoader instead of throwing UnsupportedFileTypeError — this is what empties the file content injected into the model context via filesPrompts - FileViewer: route `.v` / `.sv` into the Code renderer (extension entries plus bare tokens and text/x-* mimes in CODE_MIME_TYPES) instead of the NotSupport view - fileLanguage: map `.v` → `verilog`, `.sv` → `system-verilog`. The shiki bundled grammar id is `system-verilog`; `systemverilog` is not a bundled id/alias and would silently degrade highlighting to plaintext Tests: upload admission (empty/octet-stream/uppercase mime), HDL mime resolution with charset, loadFile content extraction from real .v/.sv fixtures, filesPrompts context injection, shiki grammar id validity, token-accounting text-like classification, and lookalike-extension negatives (.vsv/.vv). Existing behavior for other types unchanged (.zip still rejected, .pdf still uses the dedicated loader). * refactor(file): route FileViewer HDL entries through the shared verilogSupport module Address codex review on PR #19118: - FileViewer CODE_EXTENSIONS / CODE_MIME_TYPES now spread the canonical VERILOG_FILE_EXTENSIONS / VERILOG_FILE_MIME_TYPES from ./verilogSupport instead of keeping private copies, so the regression test protects the shipped routing — dropping an entry from either side fails the suite instead of silently regressing .v/.sv previews to NotSupport. - verilogSupport: the guard now matches the same text/x-* mime tokens the production router consumes, and the canonical entries are exported as the single source of truth. - uploadGuard.test: drop the duplicated upload-admission test block (identical purpose and assertions as the preceding test). * fix(file): apply chat-side subtree patch missed in previous commit * fix(file): correct corrupted verilogSupport blob (sv entry, mime list literal) * 💄 style(file): fix import order in FileViewer (simple-import-sort) CI lint failed on simple-import-sort/imports — the verilogSupport import inserted via the review fix sat above the './NotSupport' block. eslint autofix moves it to its sorted position after './Renderer/Video'. |
||
|
|
664f8582e6 |
🐛 fix(context-engine): diagnose synthetic tool failures with reason and tool (#19116)
* 🐛 fix(context-engine): diagnose synthetic tool failures with reason and tool When a tool call has no usable result at request-build time, ToolMessageReorder injects a synthetic failure payload — a bare `{"error":"Tool call failed","success":false,"synthetic":true}` — with no hint of why. Agent sessions kept hitting that opaque blob: the model cannot tell "the tool executed but the result was lost in transport (gateway 503/504, crash)" from "the tool ran and produced nothing usable", so retry decisions are made blind and the payload is undiagnosable in traces. Carry the diagnosis in the payload itself: - `reason: 'tool_result_missing'` when the tool message never arrived — the call may still have executed and a retry can be justified; - `reason: 'tool_result_unusable'` when a result row exists but carries no readable content and no pluginError; - `tool: <name>` so multi-call turns can be triaged without correlating tool_call ids. The existing `error`/`success`/`synthetic` fields are unchanged, so any consumer keyed on the old payload still matches. * 💬 refactor(context-engine): add model-readable hints to synthetic tool failures Address the codex review: the reason enum alone does not show the model knows what to do with it, and the payload is the only surface we control on this path. Each synthetic failure now carries a one-sentence `hint`: tool_result_missing warns that whether the call executed is unknown and observable state should be checked before retrying side-effecting tools; tool_result_unusable says the call ran and returned nothing readable. Hints are exported alongside the factory so tests and eval fixtures can reference the exact strings. Follow-up: a representative model-behavior eval covering both reasons belongs to the internal eval suite, not unit tests. * 🚨 fix(context-engine): narrow synthetic failure content to string in test `result.messages[1].content` is typed `string | any[]`, so passing it straight to `JSON.parse` failed `tsgo --noEmit` with TS2345. Cast to `string` like the other context-engine tests do, and apply prettier formatting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Arvin Xu <arvinx@foxmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
348c32d5bc |
♻️ refactor(goal): rename graph-node Work wording to Task (#19081)
* ♻️ refactor(goal): rename graph-node Work wording to Task Co-Authored-By: Claude Code <noreply@anthropic.com> * ♻️ refactor(goal): drop pre-release Work compatibility Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> |
||
|
|
c3aaad989d |
✨ feat(goal): structured acceptance criteria, goal chat with context injection, liveness heartbeats and process-control polish (#19050)
* ✨ feat(goal): goal-page acceptance UX — structured criteria, goal chat with context injection, liveness heartbeats and process-control polish Six review rounds of goal-page UX work, verified end-to-end against a local full stack (acceptance: six published rounds, all checks passed). ## Goal acceptance becomes structured and gating - Goal creation persists drafted criteria as verify_criteria rows and records their ids on `config.acceptance.criteriaIds` (`goal.create` input, client executor, /goal server runtime and modal all pass them through). The server guards API callers that send criteria with a bare requirement by folding them into the prose via `buildGoalRequirement`. - The terminal Goal-acceptance Work is dispatched with `verifyCriteriaIds`, so its verify plan materializes exactly the goal's criteria (deterministic checklist, edits live) instead of AI re-deriving them from prose. New `goal.setAcceptanceCriteria` + `verify.getCriteria` endpoints back the goal-page criteria section (inline edit / remove / add). - `buildGoalRequirement` now composes structured markdown (bold criterion titles, judge notes as in-item hard-break lines — a nested bullet steals the editor's list ordinals) since the requirement doubles as the page's editable markdown document. - Verify plan generation for criteria-less tasks goes through AI decomposition with the requirement as context; the hardcoded holistic check remains only as the generation-failure fallback, and its title is localized client-side (`HOLISTIC_CHECK_TITLE`). ## Goal chat with goal-context injection - The goal detail page's right panel hosts a conversation with the responsible agent. The viewed goal rides `ConversationContext` → `RuntimeInitialContext.goalOverview` on both transports (client streaming executor and gateway appContext → server `operationPrep`). - `GoalContextSyntheticInjector` (context-engine) injects the overview as a synthetic `getGoalContext` tool-call pair right after the last user message — exactly one pair per request, real tool chains kept intact (locked by tests; verified live with a real web-search turn). ## Process control & liveness - Liveness combines `goal_nodes.updatedAt` with the run operation's heartbeat (`graph()` returns `runHeartbeats`), so a task quietly inside a long tool call is no longer reported as lost while the lease is fresh. - Decision-gate cards: localized coordinator vocabulary (`coordinatorCopy.ts`), plain problem statement, attempt ledger, tidier spacing, no example sentence in the note placeholder. - Finished frontier rows display in work numbering (#1, #2) — recency only picks which rows stay. - Graph polish: `depends_on` edges drop their label; node titles for coordinator-created nodes localize through `coordinatorNodeTitleKey`. - Portal widths remember per view AND per scope (goal page vs chat), with a wider default for task/goal drill-downs; goal tabs title from the goal name; copy-link produces a shareable web URL instead of app://renderer. ## Collapsed tool rows - Collapsed tool inspectors and the running workflow headline show a plain "action + one keyword" line (`extractToolKeyword`), with the rich inspector restored when expanded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor(goal): ship structured goal-overview data; address Codex review - `RuntimeInitialContext.goalOverview` now carries the structured snapshot (goal / work / findings / pending decisions) instead of a prebuilt prompt string; `GoalContextSyntheticInjector` owns rendering it via `buildGoalOverviewPrompt`, and both transports share one `buildGoalOverviewContext` distiller. - `setAcceptanceCriteria` also rebinds the dispatched terminal Goal-acceptance task's Acceptance config, so the verifier stops materializing plans from the stale id list (regression test drives a goal to terminal dispatch and rebinds). - `verify.getCriteria` resolves each criterion's judge instruction from its linked document; the goal-page editor pre-loads it (inspectable, edited in place when unchanged) and the list shows it under the description. - The acceptance-standard section stays mounted when the criteria list is emptied (empty state + add control) — presence of the acceptance config, not a non-empty list, decides rendering. - `collectRunHeartbeats` batches operation lookups (`findByIds`) instead of one query per running topic per poll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e863261cb7 | 🐛 fix(desktop): recognize AVIF files as images (#18867) | ||
|
|
c143d887d7 |
🐛 fix(context-engine): stop reporting image tool results as failed calls (#18795)
`MessageContentProcessor` rewrites a tool result that produced images (e.g.
`readFile` on a screenshot, whose uploaded URL rides on `pluginState.images`)
into multimodal parts — `[{ type: 'text' }, { type: 'image_url' }]` — whenever
the target model supports vision.
`ToolMessageReorder`, the last processor in the pipeline, then guarded its
content-preservation branch with `typeof content === 'string'`. That guard
rejects exactly the results carrying an image: the parts were dropped and the
model received the synthetic `{"error":"Tool call failed","success":false}`
payload instead. A successful screenshot read arrived as a failed tool call and
the image never reached the request at all.
Preserve non-empty multimodal parts; the string branch keeps its existing
semantics (an empty string stays valid unless the row also recorded a
`pluginError`, in which case the error text still wins).
Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
774e04c44b | 🐛 fix: support media refs for tool result images (#18743) | ||
|
|
1dae711792 |
🐛 fix(tools): recover stale scoped tool calls (#18664)
Co-authored-by: Arvin Xu <arvin.x@lobehub.com> |
||
|
|
0887c8f5d7 |
🐛 fix(context-engine): fix unstable web document index prompt (#18630)
* 🐛 fix(context-engine): stabilize web document index hint * 🐛 fix(context-engine): stabilize user document hint * ✅ test(context-engine): preserve document discovery guidance * 🐛 fix(context-engine): preserve document test literal types * 🐛 fix(context-engine): retain document fixture literals --------- Co-authored-by: Arvin Xu <arvin.x@lobehub.com> |
||
|
|
3c64a7edb7 |
✨ feat(eval): restore test case history into run topics (#18604)
* ✨ support eval cases with topic history * ♻️ inline eval message file parsing * ♻️ narrow eval history restoration * ♻️ provision eval topics with history * ✨ add case eval environment prompts * 🐛 redirect invalid auth sessions to sign-in * ♻️ refactor(eval): separate runtime controls from prompt context * ♻️ refactor(eval): inline topic provisioning * ♻️ refactor(eval): localize history restoration * 🧹 chore(eval): remove redundant tests * 🐛 fix(eval): batch restored message parent links |
||
|
|
e41b6cb0de | ✨ feat(goal): gate work tasks with acceptance (#18623) | ||
|
|
83b41ee7fb |
✨ feat: forward eval tool calls to mock servers (#18516)
* ✨ feat: forward eval tool calls to mock servers * 🐛 fix: forward eval tools in queued workers * ✨ refactor eval tool forwarding config * ✨ feat: forward eval tool calls through hooks * 🐛 fix: preserve eval mock interception * ♻️ refactor: shrink eval forwarding test diff * 🐛 fix: type eval tool forwarding patches |
||
|
|
d04cddd3af |
✨ feat(expertise): inject operation-scoped context (#18420)
* ✨ feat(expertise): inject operation-scoped context * ♻️ refactor(expertise): colocate snapshot builder * 🚧 chore(expertise): gate context injection behind lab * ♻️ refactor(expertise): trim terminal state payloads * ♻️ refactor(expertise): move injection gate into context engine |
||
|
|
941403a556 |
chore: remove internal LOBE-XXX issue references from code comments (#18327)
* chore: remove internal LOBE-XXX issue references from code comments Rewrite code comments that referenced internal Linear issue IDs (LOBE-XXXX) into self-contained descriptions of the scenario, keeping code semantics unchanged. User-facing locale strings and LOBE_* constant names are left untouched. * chore: remove internal LOBE-XXX issue references from SQL migration comments --------- Co-authored-by: arvinxx <arvinxx@users.noreply.github.com> |
||
|
|
a58d181309 |
✨ feat(chat): support web voice messages (#17744)
* ✨ feat(chat): support web voice messages * 💄 style(chat): align voice message composer * 💄 style(chat): match voice recorder input scale * 🐛 fix(chat): animate voice message waveform * 💄 style(chat): preserve composer height during recording * 🐛 fix(chat): ignore inherited voice send hover * ✨ feat(chat): normalize voice messages across providers * 🐛 fix(chat): make voice message retries cancellation-safe * 🐛 fix(chat): restore LobeHub voice capability * 🐛 fix(chat): make voice messages optimistic and context-safe * 🐛 fix(chat): drop obsolete topic loading cleanup * 🐛 fix(chat): align voice capability gating * ✅ test(chat): fix voice context typing * 🔨 chore: resolve canary rebase conflicts * ✅ test(database): align audio metadata query expectations * 🐛 fix: block voice recording while generating |
||
|
|
4bf7040515 |
✨ feat: inject recent same-channel history & trim unsupported IM message APIs (#16608)
* ✨ feat: inject recent same-channel history & trim unsupported IM message APIs On IM platforms without a history-read API (e.g. WeChat), the agent had no way to answer "what did we just talk about": the system prompt told it to call `readMessages`, which WeChat rejects with PlatformUnsupportedError, so the model apologized instead of answering. - Dynamic manifest: add `resolveMessageManifest` (mirrors `resolveLobeAgentManifest`) so the `lobe-message` tool drops APIs the current IM platform can't fulfil, in both the tool list and systemRole. Platforms declare `unsupportedMessageApis` on their PlatformDefinition; WeChat lists the ops its service throws on. - Pre-inject recent same-channel history: for platforms that can't read history, surface recent topic titles + the last few user messages from the same channel (matched via `metadata.bot.platformThreadId`, current topic excluded) into the bot platform context, and stop instructing the model to call `readMessages`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix: cover all no-history IM platforms & include absent optional message APIs Address review: Telegram/QQ also lack a readMessages API and every platform except Discord lacks sendDirectMessage, but only WeChat was annotated — so those bots still got the "call readMessages" guidance, skipped history pre-injection, and were handed dead tools. - Centralize per-platform unsupported message APIs into `PLATFORM_UNSUPPORTED_MESSAGE_APIS` (single source of truth), derived from each service's throws AND its absent optional methods (e.g. sendDirectMessage). Wire it into every limited platform definition: telegram, qq, slack, feishu, lark, imessage, wechat. - WeChat now also trims sendDirectMessage; Telegram/QQ now report canReadHistory=false and get recent-channel-history injection. - Add a drift-guard test that derives the real support from the service sources and asserts the map matches, so a service change that isn't mirrored fails CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✅ test: snapshot the full botPlatformContext prompt output Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor: inject recent channel history as structured <recent_topics> with lh topic view escape hatch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor: inject recent topics only when opening a new topic, cap at 3 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
cd474dfcc3 |
✨ feat(model-runtime): estimate audio input costs (#17949)
* ✨ feat(model-runtime): estimate audio input costs * 📝 docs(model-runtime): clarify audio usage comments * 🐛 fix(model-runtime): avoid ambiguous cached audio billing * 🐛 fix(file): persist uploaded audio duration |
||
|
|
86991beb75 |
♻️ refactor: inject graph runtime context continuously (#17761)
* ♻️ refactor: inject graph runtime context continuously * ♻️ refactor: clarify graph node tool guidance * ♻️ refactor: generalize runtime additional context * ♻️ refactor: simplify additional context injection * ♻️ refactor: tighten runtime context provider * ♻️ refactor: group graph agent files * ♻️ refactor: separate graph context hooks * ♻️ refactor: compact graph prompt hook application * ♻️ refactor: share JSON-safe conversion utility * ♻️ refactor: keep runtime context at virtual tail * ✨ refactor runtime additional context placement * ♻️ remove unused virtual tail override * 📝 rename runtime additional context types * 🧹 clean up runtime executor context fixture * ♻️ tighten runtime additional context contract * 🧹 clean up runtime executor test diff * 🧹 drop unnecessary context phase comment diff |
||
|
|
cc064ee9b1 | 🐛 fix(context-engine): use absolute dates in agent documents index (#17981) | ||
|
|
5b348e814c | 🐛 fix(context-engine): carry dynamic activation docs to the LLM payload once (#17972) | ||
|
|
43504be18f |
🧹 chore: clean LOBE-XXX code markers (2026-08-04) (#17890)
🧹 chore: clean LOBE-XXX markers from source code
- Replace LOBE-12572 references with descriptive context about assistant prefill poisoning
- Replace LOBE-12593 references with context about view-mode toggle scroll bug
- Replace LOBE-12381 references with context about BM25 index fast fields
- Remove @see links to resolved Linear issues (LOBE-12379, LOBE-12575)
- Remove LOBE-XXX markers from test descriptions
|
||
|
|
cacf310895 |
🐛 fix(context-engine): account container message tokens in context budget (#17839)
* 🐛 fix: count assistantGroup children in context token accounting * 🐛 fix: account assistant group tool tokens * 🐛 fix: account tasks/groupTasks/agentCouncil tokens in context accounting Extend countContextTokens to mirror the flatten processors that restore container messages into the provider payload: - tasks/groupTasks: count each task's content + metadata.instruction (TaskMessageProcessor re-emits them inside one assistant template) - agentCouncil: count members (plain assistant or nested assistantGroup), including tool results and tool.id as tool_call_id - standalone role:'task' messages use the same accounting as one entry of a tasks container Without this, subagent/broadcast conversations undercount the context budget, so compression triggers too late and risks overflowing the model's context window. UI-only denormalizations (in-bubble council blocks, taskCompletions) stay uncounted: GroupMessageFlatten drops them and they never reach the provider. * 🐛 fix: count task template overhead in token accounting TaskMessageProcessor re-emits task messages inside the instruction/result template ([Task Result from {{agentName}}] + headings). countTaskMessage only estimated content + metadata.instruction, missing the static headings and agentName — every historical task message undercounted by ~20 tokens, accumulating in long subagent/broadcast conversations and delaying the compression trigger. Export DEFAULT_TEMPLATE plus a shared formatTaskMessage helper from TaskMessage.ts (single source of truth, processor now delegates to it) and make countTaskMessage estimate the exact formatted text the processor emits, including agentName (agentName || meta.avatar || 'Sub Agent'). Instruction-less tasks keep their previous content-only accounting, which already matched the processor's plain-assistant fallback. |
||
|
|
a27dfaeda1 | ✨ feat: support audio in multimodal understanding (#17904) | ||
|
|
353f000062 |
✨ feat(agent): give new agents a personal name (#17853)
* ✨ feat(agent): give new agents a personal name Wire the `agents.name` column (added in #17821) through the type, store and Agent Builder layers, and seed every newly created agent with a common given name in the user's language. `name` is the agent's personal identity ("Alice", "小艾"); `title` stays the role it plays ("Health Assistant"). Display semantics are unchanged — the app still labels agents by `title` everywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent-builder): make a missing agent name visible in the injected context Acceptance testing found the builder configuring a legacy nameless agent — title, description, avatar, tags, system prompt — and silently skipping the name, because "this agent has none" was expressed by omitting the `<name>` element entirely. Asking the model to notice an absent tag is a much weaker signal than showing it an empty value. Always emit `<name>`, empty included, and make filling an empty one a required part of Step 1 rather than something to do "proactively". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(agent): show agents by their personal name across the app The `name` column was populated but invisible: every surface still labelled agents by `title`, so a seeded or user-chosen name never appeared anywhere. Return both fields wherever an agent is queried for display, and resolve the label through one helper — `agentDisplayName(agent, fallback)` = name, else title, else the caller's fallback (blank-but-present values count as absent). - data layer: `name` travels alongside `title` out of every agent-shaped query (sidebar, search, rank, messenger picker, group members, briefs, shares, nightly review) and in the DTOs built on them - display: ~40 call sites switch to the helper — sidebar, chat message authors, command menu, mentions, forward/move/assign pickers, portals, breadcrumbs, route titles, desktop notifications, tray and screen-capture snapshots - editing surfaces keep binding to raw `title`; the agent profile header gains a labelled name field so the value driving all of the above is reachable Display semantics only — no query, permission or write path changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ♻️ refactor(agent): carry name and title through DTOs instead of collapsing them `useAgentMeta` returned a `MetaData` whose `title` had been overwritten with the resolved label, so a consumer asking for the agent's role got its personal name instead. Same shape in a few other DTOs that cross a layer boundary. Keep both fields on anything that represents an agent, and resolve to a single label only where one is actually rendered: - `useAgentMeta` returns the meta untouched; `ChatItem` / its `Title` and `Avatar` resolve at render. The supervisor row drops `name` alongside its title override, so a member's name can't outrank the group label - `AgentAvatarInfo`, `TaskDetailActivityAgent`, `SharedTopicData.agentMeta` (and its members), `AgentBoundConnector` gain `name`, populated server-side - `listMessengerBindableAgents` returns `title` raw again; the text-only bot channel resolves it, since nothing downstream of it can - the supervisor prompt builder takes both fields and resolves them itself Local view-models built for immediate rendering (pickers, route titles, native menu labels) keep resolving inline — that is the consumer doing its job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): label the active agent by its name in the shell and mobile header Acceptance testing found the same agent labelled two ways at once: the message author read "小艾" while the sidebar header above it still read "健康助手". The display sweep converted components that read agent meta directly but missed `agentSelectors.currentAgentTitle`, which five shell surfaces render. Add `currentAgentDisplayName` (name, else title) and point the display readers at it — agent sidebar header, mobile chat header, and the three share-image composers. `currentAgentTitle` stays the raw role for anything that edits it. `currentAgentMeta` also gains `name`, matching `getAgentMetaById`. Also fix the profile name field's i18n: the locale JSONs use flat dotted keys, so the nested object added with the field never resolved — the label rendered as English "Name" on a Chinese UI, and a stray `settingAgent` object was left in both files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): lead with the name and keep the role visible beside it Review feedback on the display migration: replacing the label with the name hid the role entirely, and the profile still led with the role. - profile header: the personal name becomes the headline, the role moves under it behind a 角色 / Role label, and the url slug renders read-only beside it - sidebar rows, the shell conversation header and the chat message author show the role as a muted tag next to the name — only when the name is what took the label, so a role-labelled agent never repeats itself - `ChatItemProps.avatar` is widened to carry `name`: the `MetaData` it borrows from `@lobehub/ui/chat` predates the name/title split Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(agent): open an agent by its slug and edit its identity inline Review feedback on round 4: - `/agent/:slug` now resolves user-chosen slugs, not just builtin ones. The lookup is ownership-scoped and returns null for both "unknown" and "not yours", so it can't be used to probe which slugs exist. A route param is treated as a slug only when it has no underscore — every generated agent id has one, so an ordinary id route costs no request. - slug becomes renameable through its own validated endpoint (shape, reserved builtin slugs, uniqueness in the same scope as the db indexes). It stays in `IMMUTABLE_AGENT_FIELDS` so it can never ride along in a config patch. - the profile identity block is read-only until a pencil is clicked, then offers all three fields — name, role, slug — grouped on one line instead of the slug sitting at the far edge. - the injected `<name>` falls back to the role rather than an empty marker; the builder no longer treats a nameless agent as a hole to fill. - the shell header drops its role tag (the message row already carries it) and the message-author role becomes a filled Tag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): stack the identity editor and bring its confirm within reach Review feedback on round 7: in edit mode the role and slug shared one line with the confirm stranded at the far right edge of the header, and the slug read dimmer than the role beside it. Give the role and the slug a line each with fixed-width labels so their inputs align, put the confirm directly under them as a labelled primary button instead of a faint icon at the edge, and pin the slug input to the normal text colour so it carries the same weight as the role it sits under. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): move identity editing into a form modal Review feedback on round 8: the inline editor was still cramped — three fields sharing the header left no room for a label or an error per field — and the slug should carry the secondary text colour rather than the primary one. Open a form modal from the pencil instead. The header goes back to display-only (name, then role · @slug), and the modal gives each field its own labelled row with its own hint/error line. Behaviour lives in `useAgentIdentityForm`, so it is covered by a hook test rather than a component test. The slug still commits through its own validated endpoint, and it commits BEFORE the meta patch: a rejected slug must not leave name and role persisted behind a form the user still has to fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): put the slug text on the secondary colour, not the wrapper's The previous attempt set `color` via `style`, which lands on the affix wrapper — the inner `<input>` has its own colour rule and ignored it, so the slug rendered at whatever antd defaulted to while only the `@` picked the value up. Target the semantic slots instead: the slug text takes the secondary colour so it sits one step below name and role, and the `@` keeps the tertiary one. Measured in the browser (light theme): role rgb(8,8,8), slug rgb(102,102,102), `@` and the hint rgb(153,153,153). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): lift the profile's role and slug line to the secondary colour The last two rounds coloured the wrong surface — the review was about the read state under the agent name, not the edit form. Revert the form to its default input colours and fix the line that was actually meant. `Text type="secondary"` resolves to `colorTextDescription`, which antd maps to the TERTIARY step, so the role and slug were rendering as faint as the separator between them. Set the secondary colour explicitly and leave only the decorative `@` and `·` at tertiary. Measured in the browser (light theme): name rgb(8,8,8), role and slug rgb(102,102,102), `@` and `·` rgb(153,153,153). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): show personal names in the agents list canary added Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✅ test(agent): cover the name field in the agent avatar DTO Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): keep a builtin agent's slug immutable A builtin agent IS its slug — `getBuiltinAgent` resolves it by that string. Renaming one away used to succeed, which minted a second empty inbox on the next lookup and stranded the original's history on an ordinary agent. The reserved-slug guard also ran before the no-op check, so a builtin agent re-submitting its own slug came back as `reserved` — saving the identity form on Lobe AI failed on a field the user never touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ✨ feat(agent): offer one-click naming for an agent with no name The profile headline fell back to the name field's PLACEHOLDER, so every unnamed agent read as though it were literally called "Give it a name, e.g. Alice". It now falls back to a plain unnamed label, and the prompt to fix it gets its own line with a one-click draw from the same name pool a fresh agent is seeded from. Names already visible in the sidebar are excluded from the draw — a second "Alice" would defeat the point of having a name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(agent): refresh the sidebar after an identity rename The sidebar list holds its own copy of an agent's label, so renaming from the profile left the new name on the profile and the old one in the list until something else revalidated. The sidebar's own rename popover already refreshes; the profile path (identity modal + one-click naming) now does too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 📝 docs(acceptance): record the duplicate-dep ErrorBoundary diagnosis Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): state a missing role instead of borrowing another field The profile headline is the name slot, so it no longer falls back to the role: an agent titled "Lobe AI" printed that string twice in adjacent lines. A missing name says so, and a missing role now says so too rather than collapsing the line to a bare slug. A builtin agent's identifier stops pretending to be an editable field — a disabled input reads as "you can't use this control right now" and needs a sentence explaining itself, where the bare @inbox marker states the fact. The agents list gets the role back beside a personal name, matching the sidebar. Also guards the profile Header against reading an unloaded config: its useCallback dependency array evaluates during render, so reaching a profile by slug — one resolution hop slower — dropped the whole page into the error boundary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): give the headline slot to the fix, not a placeholder With no name there is nothing to headline, so the slot carries the one thing that resolves it — the prompt and its one-click naming — instead of a label pretending to be a name. The edit affordance stays hidden until then: naming it IS the next step, and a second path to the same thing would only split attention. Both come back once the agent has a name. Read-only viewers still get the plain unnamed label; an action they cannot take would be worse than a stated absence. The agents list now marks the role with the same filled Tag the message author row uses, replacing a half-transparent bare span. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 💄 style(agent): name the real url in the slug hint A literal /agent/<slug> leaves the reader to do the substitution. The hint now shows the url the current input actually produces, and updates as they type; an empty field still falls back to describing the field in the abstract, since there is no url to name yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ba69b32909 |
🐛 fix(model-runtime): support advanced reasoning parameters for Cerebras and Groq (#16469)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com> |
||
|
|
b6a8fcf0dc | 🐛 fix: filter failed assistant placeholders and harden Claude prefill guard (#17737) | ||
|
|
4e677af923 |
🐛 fix(agent): inject runtime todos into server context (#17671)
* 🐛 fix(agent): inject runtime todos into server context * 🧪 test(agent): keep server todo fixture canonical |
||
|
|
b36a003ac5 | 🐛 fix(model-runtime): sanitize replayed thinking history for anthropic-compatible providers (#17629) | ||
|
|
3384fe491e | ✨ feat: add workspace topic comments (#17537) | ||
|
|
70f0b34d2d |
✨ feat: support ChatGPT subscription authentication (#17527)
* ✨ feat: support ChatGPT subscription authentication * 🐛 fix: preserve Codex reasoning state and client identity * 🐛 fix: preserve reasoning source in agent state * 🐛 fix: require ChatGPT account identity * 🐛 fix: support Codex Responses Lite requests * 🐛 fix: apply Responses request preparation consistently * 🐛 fix: preserve server reasoning signatures * 🐛 fix: enable ChatGPT reasoning replay * 🐛 fix: preserve non-streaming reasoning signatures * ✅ test: fix ChatGPT runtime type compatibility * ♻️ refactor: type Responses reasoning provenance |
||
|
|
a6a2642008 |
🐛 fix(tools): make TOOL_NAME_MAX_LENGTH=0 actually disable tool-name compression (#17530)
* 🐛 fix(tools): make TOOL_NAME_MAX_LENGTH=0 actually disable tool-name compression `TOOL_NAME_MAX_LENGTH` (#17249) is read from `process.env` at module load in `ToolNameResolver`, so it only ever applied to the gateway (server-run) path. The default client-driven chat path builds the tool payload in the browser, where the server env is invisible — deployments setting `0` still got opaque `MD5HASH_…` tool names. Ship the value to the client with the global server config and push it into the context-engine module both on config load and at `createToolsEngine` (the client mirror of `createServerToolsEngine`), before any tool name is generated. Also adds the var to the `@lobechat/env` tools schema and documents it in the self-hosting env docs (en + zh-CN). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ♻️ refactor(tools): parse TOOL_NAME_MAX_LENGTH through one shared function Review follow-up: the env schema coerced the value with zod while `ToolNameResolver` parsed `process.env` with `parseInt`, so the same env value could mean two things on the two sides that generate tool names: raw resolver (server) shipped to client 1e2 1 100 " " 64 (fallback) 0 → compression silently off on the client 2048 2048 throws -5/abc 64 (fallback) throws The last row is the worst: `@t3-oss/env-core` throws on validation failure, so a typo'd value would take `getServerGlobalConfig()` down entirely, where before it just fell back to 64. Move the parse into `parseToolNameMaxLength` (`@lobechat/const/plugin`, a leaf module both sides can import cheaply — importing context-engine here would drag the database module graph into the config path) and have the resolver and the global config both use it. Unset / unparseable / negative reads as "not configured" and falls back to the default; no upper bound, matching the resolver. Env schema now passes the raw string through. Covered by a table test in `ToolNameResolver.config.test.ts` (parse semantics) and one in `getServerGlobalConfig.test.ts` (the shipped value matches, and a typo no longer throws). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ♻️ refactor(tools): keep the tool-name config out of the app-shell bundle The client-side sync lived in `serverConfig`'s SWR success handler, which put a static `@lobechat/context-engine` import into a store that every page creates — including the lightweight auth / popup shells — dragging the engine's module graph (down to `@lobechat/database`) into bundles that never generate a tool name. Keep the value as plain data in the server config and push it into the engine from the sites that actually generate names, via `applyToolNameMaxLength()`: `createToolsEngine` (the tools payload) and the agent-profile mention list, whose generated names are written into the system prompt and so must match the names sent to the model. `src/store/serverConfig` is now byte-identical to canary. Verified: SPA vite build passes (the `@lobechat/const/plugin` subpath resolves in the client bundle); context-engine 445 tests, client-side 286 tests, server globalConfig/tool-engine suites, and the full type-check all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b35c96c3e9 |
🐛 fix(agent): guide visual fallback for text-only models (#17466)
* 🐛 fix(agent): guide visual fallback for text-only models * 🐛 fix: unblock visual fallback PR checks |
||
|
|
11842c86de |
✨ feat: make tool-name MD5 compression length configurable (#17249)
* ✨ feat: make tool-name MD5 compression length configurable Tool names >= 64 chars are compressed to an opaque MD5 hash because OpenAI caps function names at 64. But this ran unconditionally for every model, and the hash hurts readability — models that don't cap name length pay the cost for nothing. Add `TOOL_NAME_MAX_LENGTH` (env) as the compression threshold. `0` disables length-based compression entirely, so deployments whose models don't need it get full, readable tool names. Default stays 64 → no behavior change unless set. Since context-engine can't read env, the value is pushed in via setToolNameMaxLength() from the server tool-engine boundary. Invalid-character normalization is independent and always applies. resolve() already handles both compressed and uncompressed names, so roundtrip is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 🐛 fix: read tool-name length from env at module load, not per-engine Codex review: setToolNameMaxLength() only ran inside createServerToolsEngine, so resume / cold-start paths (ServerContextBuilder, ToolResolver, GraphAgent) that reach generate()/resolve() in a fresh worker without creating a tools engine fell back to the default 64. If the persisted operationToolSet.tools were built with a different threshold (e.g. 0 = no compression), the regenerated names would mismatch and allowedToolNames filtering / prompt injection could drop long tools. Read TOOL_NAME_MAX_LENGTH directly at ToolNameResolver module load instead, so every worker resolves the same threshold regardless of init order. Guarded for non-Node runtimes. Drops the now-redundant env-schema entry and per-engine setter wiring; setToolNameMaxLength stays as a test/host override. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f945c00d50 | ♻️ refactor(lint): clean up eslint-disable directives (#17206) | ||
|
|
1577b79890 |
✨ feat(agent-tracing): ctx-lint context quality linter + golden payload tests (#17113)
* ✨ feat(agent-tracing): add ctx-lint context quality linter + golden payload tests - packages/agent-tracing/src/analysis/contextLint.ts: deterministic lint rules over the assembled LLM payload (contextEngine.output), token-weighted findings + per-op feature rollup (candidate agent_operations analytics shape) - agent-tracing ctx-lint CLI subcommand (single op / corpus batch / JSONL features) - packages/context-engine: golden payload regression tests pinning MessagesEngine output for fixed scenarios with budget & structure invariants Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * 🐛 fix(agent-tracing): stop classifying mixed-content results as tool failures Head-regex error detection misfired on partial-success crawl results and code/doc content that merely mentions errors (10/10 sampled 'large errors' were isSuccess=true). Trust order is now: explicit isSuccess flag > structured error containers > head regex (short unflagged outputs only). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ✅ test(agent-tracing): make ctx-lint tests type-check under repo tsgo - switch bun:test imports to vitest (repo-standard, typed via workspace devDep) - add minimal vitest config + test script to agent-tracing so its tests don't pick up the root app setup - golden test msg() fixture helper accepts OpenAI-shape fields (tool_calls) that UIChatMessage doesn't declare, matching MessagesEngine.test.ts casts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3c3dcd608c |
🐛 fix(agent-runtime): restore activated tools across operations (#17076)
* 🐛 fix(agent-runtime): restore activated tools across operations * ✅ test(agent-runtime): cover cross-operation tool restoration * 🐛 fix(agent-runtime): gate restored tools by current run * 🐛 fix(agent-runtime): expose restoration eligibility in service types * ✅ test(agent-runtime): preserve existing tool generation contract |
||
|
|
8284a6e2b2 |
✨ feat(local-system): let readFile return images so vision models can see them (#16912)
* ✨ feat(local-system): let readFile return images so vision models can see them Tool-returned images previously reached the LLM only as `` text — the model never actually saw them. Wire the builtin readFile path so an image file becomes a real `image_url` content part (LOBE-6801 builtin path; mirrors CC Read-on-image LOBE-7786 and MCP resource image #13618). - context-engine MessageContent: new processToolMessage reads pluginState.images → image_url parts (vision) or a downgrade placeholder, keeping tool_call_id so providers can pair the result with its call. - tool-runtime: ComputerRuntime.readFile detects isImage → state.images; LocalSystemExecutionRuntime normalizeResult passes isImage/previewUrl; ReadFileState gains images (+ ToolResultImage type). - desktop LocalFileCtr.readFile: image branch resolves a desktop preview URL via localFileProtocolManager (local-file-shell refuses binary), so DB stores a small URL instead of base64; LocalReadFileResult gains isImage/previewUrl. - ReadLocalFile renderer: PreviewGroup thumbnails when state.images present. Desktop-only for now; CLI/cloud-sandbox readLocalFile still refuses binary. Co-Authored-By: Claude <noreply@anthropic.com> * ♻️ refactor(local-system): upload readFile images instead of localfile:// preview URLs The previous approach persisted desktop-only `localfile://` preview URLs (5-min in-memory tokens) into pluginState.images and expected the server-side send path to fetch them — neither holds. Rework the chain to the same convention as the hetero CC Read-on-image echo (#16786): - LocalFileCtr.readFile returns the image bytes as base64 (`imageData`), dropping createPreviewUrl and the dirname(path) workspaceRoot guess that rejected any image nested below an approved workspace root; paths now resolve via local-file-shell's resolveAgainstCwd. Formats limited to png/jpeg/gif/webp (what vision providers accept; svg reads as text) with a 10MB cap. - ComputerRuntime routes the bytes onto state.images as pre-upload `{ data, mediaType }` entries; the renderer uploads them to file storage (uploadToolResultImages) and rewrites to `{ fileId, url }` BEFORE persistence, degrading to the `[Image: …]` placeholder on failure. The gateway path strips the bytes (no upload client in main yet, same deferral as #16786). - MessageContent.processToolMessage only forwards entries with fetchable http(s)/data URLs. - OpenAI builders no longer emit image parts inside tool messages (the API rejects them): Chat Completions flattens the tool message to text and re-attaches images as a user message after the contiguous tool batch; Responses keeps function_call_output textual and appends an input_image user item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor(desktop): upload readFile images from the main process Move the upload out of the renderer persistence hook into main, mirroring the CLI's uploadFileBuffer (hash dedup via file.checkFileHash → pre-signed S3 PUT → file.createFile) over the same raw Oidc-Auth fetch pattern GatewayConnectionCtr already uses for agentNotify/device.register. - New RemoteFileUploadService (main): buffer → durable { id, url }; declines when no active remote session. - LocalFileCtr.readFile uploads right after reading the bytes and returns imageFileId/imageUrl — base64 never crosses IPC; on upload failure the content degrades to an "[Image: …] (upload unavailable)" placeholder. - ComputerRuntime emits already-uploaded state.images entries; ToolResultImage loses the pre-upload `data` field (url required again). - Drop the renderer-side uploadToolResultImages step and the gateway imageData strip — the gateway path now gets working image references too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor(desktop): delegate readFile image upload to the embedded CLI Replace the hand-rolled raw-fetch TRPC upload flow with `lh file upload <path> --json` — the CLI already owns the whole pipeline (hash dedup, pre-signed S3 PUT, file record). Run its embedded script with the app's own binary via ELECTRON_RUN_AS_NODE (what the generated wrapper does), execFile with an args array so the agent-controlled path never touches a shell, and inject the desktop session through LOBEHUB_JWT/LOBEHUB_SERVER like CliCtr and the hetero spawn paths; without a session lh falls back to its own login. LocalFileCtr now passes the resolved path instead of reading bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a5b1e52641 | 🐛 fix: runtime unresolved tool calls (#17044) | ||
|
|
e8af5258a7 |
♻️ refactor(Agent): Configurable graph agent (#16944)
* ♻️ refactor: rebuild graph agent runtime * ✅ test graph runtime integration paths * 🐛 fix(agent-runtime): stabilize graph agent tests * ✅ test: fix agent settings content mock * 🐛 fix graph prompt value typing * 🐛 fix(agent-runtime): preserve dynamically activated tools * ✅ test: use offered web browsing tool name * 🔧 remove graph runtime feature flag gate * 📝 restore internal flag comment |
||
|
|
1b21f5877a |
feat(agent): three-state (pinned/auto/disabled) plugin config (#16924)
* ✨ feat(agent): add three-state plugin config types & helpers (LOBE-11625)
Introduces AgentPluginMode ('pinned' | 'auto' | 'disabled') and
AgentPluginEntry (string | AgentPluginConfigItem) plus pure read/write
helpers (parsePluginEntry, getPluginMode, getPinnedPluginIds,
getDisabledPluginIds, getActivePluginIds, upsertPluginMode) in
packages/types/src/agent/pluginConfig.ts.
This is infra only — no existing consumer is touched yet. Legacy string
entries in agents.plugins stay untouched until an identifier is explicitly
upserted (lazy per-item upgrade), so mixed-shape arrays are expected
permanently, not as a transitional state.
* ✨ feat(agent): enforce disabled plugins at runtime tool discovery (LOBE-11626)
Replaces raw `agentConfig?.plugins` reads with the LOBE-11625 helpers at every
runtime boundary that decides which tools a model can actually call:
- Client legacy runtime (agentConfigResolver.ts) and client tools engine
(toolEngineering/index.ts) now resolve pinned-only identifiers.
- Server gateway runtime (aiAgent/index.ts, the default path) normalizes
agentConfig.plugins to pinned ids once after load, and — critically —
filters the installedPlugins/LobeHub-Skill/Composio/connector manifest pools
by the agent's disabled set before they reach the tools engine. Without this,
a disabled plugin would still be discoverable/activatable by the auto
activator, since createEnableChecker's explicit-activation bypass
short-circuits before its rule-map is even consulted.
- The LLM-editing context builder (callLlm.ts) and the installed-skills VFS
provider (ProviderSkillsInstalledActive.ts) get the same treatment.
Pure string-array plugin configs are unaffected (all existing suites pass
unchanged); new tests cover mixed-shape arrays with a disabled entry.
* ♻️ refactor(agent): upsertPluginMode removes the entry on mode='auto' (LOBE-11625)
'auto' is the implicit default for an identifier absent from `plugins`
altogether, so persisting an explicit `{ identifier, mode: 'auto' }` record
was redundant bloat and diverged from the pre-tri-state behavior where
unpinning spliced the id out of the array entirely. Setting mode to 'auto'
now removes the matching entry (string or object) instead, needed so
togglePlugin's unpin path (LOBE-11628) keeps shrinking the array like before.
* ✨ feat(agent): three-state plugin write path via upsertPluginMode (LOBE-11628)
togglePlugin now writes through upsertPluginMode instead of raw array
splice/push, so pinning an identifier upgrades it to object shape
(preserving every other untouched entry's original shape). Adds
setPluginMode(id, mode) for explicit pinned/auto/disabled writes — the
write-side primitive the Skills panel's three-state UI needs, since
togglePlugin's boolean signature only ever expresses pinned vs. not-pinned.
* ✨ feat(agent): resolve pinned-only ids in the shared plugin selectors (LOBE-11628)
getAgentPluginsById and currentAgentPlugins now route through
getActivePluginIds instead of returning agentConfig.plugins raw, so every
downstream consumer (Skills panel, TokenTag estimation, ShareImage preview,
ToolAuthAlert, SkillStore install-state checks) sees pinned identifiers only
— matching the pre-tri-state semantics where array-membership meant pinned,
and keeping disabled entries invisible to code that isn't tri-state-aware yet.
Simplifies the toolEngineering/index.ts call site added in LOBE-11626 now
that the selector itself does the filtering.
* ✨ feat(chat): three-state (pinned/auto/disabled) Skills panel UI (LOBE-11628)
Implements the confirmed Option B presentation: disabled plugins stay in the
Auto group (no separate group) — grouping still splits only on pinned vs
not-pinned — but render greyed out with a "Disabled" tag. The right-click/
"..." policy menu gains a third option so pinned/auto/disabled can be freely
switched between, backed by the new setPluginMode store action.
- SkillPolicyMode aliases AgentPluginMode (pinned/auto/disabled).
- disabledIds/disabledIdSet derived from the raw (unfiltered) plugins config,
since the shared getAgentPluginsById selector now returns pinned-only ids.
- renderPolicyMenu's mode detection and updateSkillPolicy both become
three-way; the two "orphaned entry lingers in config" checks (Composio
server list) now also account for a disabled entry, not just pinned.
- Adds tools.activation.disabled(.desc) strings (en-US/zh-CN + default).
* ✨ feat(agent): tri-state plugins support in the Agent Group pipeline (LOBE-11627)
Agent Group has its own zod schema + type-assertion pipeline, independent of
the main agent create/update path — it was still hard-coded to
z.array(z.string()) and `as string[]`, which would reject a group member
agent seeded with a pinned/disabled object entry.
- Adds a shared AgentPluginEntrySchema (packages/types) so any router that
accepts a caller-supplied plugins array can validate the tri-state shape
instead of rejecting object entries.
- agentGroup.ts's agentMemberInputSchema and createGroupWithMembers'
supervisorConfig schema now use it; the `as string[]` casts bridging to the
still-string[]-typed DB layer become `as unknown as string[]` (documented,
deliberate — widening the column type is deferred to the final phase).
- SupervisorAgentConfig.plugins widens to AgentPluginEntry[] to match.
- The two verbatim-copy sites (buildCopiedAgent, virtual member duplication)
need no change — copying an already-mixed-shape array is naturally
compatible.
* ✨ feat(agent): tri-state-safe writes in the LLM self-invoked tools (LOBE-11627)
agentManagement.installPlugin and agentBuilder.updateConfig/installPlugin
(the tools the LLM calls on itself via agent-management / agent-builder) all
wrote plugins with raw `(agent.plugins as string[]) .includes/push` logic.
Against a mixed-shape array this both fails to detect an existing disabled
object entry (`.includes(identifier)` never matches an object) and, on
"install", pushes a duplicate bare-string identifier alongside it instead of
flipping its mode back to pinned.
Switches all four write sites to getPluginMode/upsertPluginMode:
- agentManagement.installPlugin
- agentBuilder.updateConfig's togglePlugin handling (enabled ↔ pinned/auto)
- agentBuilder.installPlugin's two branches (official builtin tool, market
MCP plugin)
Also improves agentManagement.getAgentDetail's plugin summary to annotate
non-pinned entries (e.g. "plugin-b (disabled)") instead of joining a
possibly-mixed array as if every entry were a plain string.
Adds installPlugin coverage to agentManagement's existing test file, and a
new agentBuilder.test.ts (none existed) covering the disabled-entry scenario
for both togglePlugin and installPlugin's two source branches.
* ✨ feat(agent): widen LobeAgentConfig/AgentItem/CreateAgentSchema.plugins to AgentPluginEntry[] (LOBE-11629)
The final step of the tri-state rollout — deliberately last, since widening
earlier would have let `.includes()`/`.push()` call sites keep compiling
without ever surfacing the consumers that still needed manual migration.
Fallout handled in this commit (mechanical, no behavior change beyond
bridging types):
- aiAgent/index.ts's builtin-runtime-merge block now threads a local
`activePluginIds: string[]` through instead of repeatedly re-reading/
writing `agentConfig.plugins` (whose declared type is now the wider
union) — avoids a cast on every line, single write-back at the end.
- DB-layer AgentItem/NewAgent (packages/database/src/schemas/agent.ts) is
intentionally NOT widened — the JSONB column's `$type<string[]>()` stays
as compile-time documentation only. Every call site that bridges a
domain-level (now AgentPluginEntry[]) value into a DB-model call gets a
documented `as unknown as string[]` cast: agent.ts's createAgent router,
agent/index.ts's updateAgentConfig, session.ts's createInbox, and the
deprecated data-importer's legacy agent insert.
- agentEvalRun's snapshotAgentConfig now projects to pinned-only ids via
getActivePluginIds instead of passing the raw field through —
EvalRunAgentSnapshot.plugins stays `string[]` (it's a display-only DTO,
not a domain type) and this also fixes a real bug: RunHeader's plugin tag
list would have rendered a disabled entry as "[object Object]".
* ✨ feat(agent): fix parallel plugin-write/display consumers surfaced by widening (LOBE-11629)
Widening LobeAgentConfig/AgentItem.plugins in the previous commit turned
these into real compile errors, surfacing consumers the earlier phases'
sweep had missed — every one of them had the same raw
`.includes()`/`.push()`/`.filter()` bug as the ones already fixed:
- packages/agent-manager-runtime/AgentManagerRuntime.ts: the CLIENT-side
mirror of agentManagement.ts/agentBuilder.ts (used when gateway mode is
off) — togglePlugin, enablePluginForAgent (shared by all 9 installPlugin
call sites), and getAgentDetail's plugin summary all had the exact same
bugs already fixed server-side in LOBE-11627.
- AgentSetting/store/reducers/config.ts: a third, independent togglePlugin
implementation backing the full Agent Settings modal (opened from the
profile Header) — previously `@ts-ignore`'d into silence.
- ProfileEditor/AgentTool.tsx: a fourth parallel plugin panel (group member
editor). Also fixes its stale-plugin cleanup effect, which would have
wholesale-dropped disabled entries from the array since it filtered on
the already pinned-only `plugins` local instead of the raw config.
- contextEngineering.ts / agentByIdSelectors.getAgentBuilderContextById:
display DTOs for Group Agent Builder / Agent Builder prompt context now
project to pinned-only ids instead of passing the raw field through.
- onboarding/action.ts's toggleInboxAgentDefaultPlugin: same push/splice bug
for the onboarding flow's default-plugin toggle.
All writes now go through getPluginMode/upsertPluginMode; tests added for
each fixed write path's disabled-entry scenario.
* 🐛 fix(agent): exclude disabled plugins from the profile markdown export (LOBE-11629)
Resolves the "待复核" finding from LOBE-11624's sweep: this file does touch
agent.plugins. buildAgentProfileMarkdown joined the raw plugins array as
plain strings for the "Enabled Plugins" section — with a mixed-shape array
a disabled entry would render as "- [object Object]". Switches to
getActivePluginIds so the export only lists what's actually pinned.
* 🐛 fix(agent): disabled composio/skill still resurfaced in <available_tools>/<available_skills>
Two gaps in the LOBE-11626 runtime enforcement, found via user report (a
disabled Composio integration and a disabled skill both still showed up as
"available but not activated" in the model's context):
- aiAgent/index.ts's activator-discovery ingest ("Include lobehub skill and
composio manifests for activator discovery") populated toolManifestMap
from the RAW lobehubSkillManifests/composioManifests arrays instead of the
already disabled-filtered activeLobehubSkillManifests/activeComposioManifests
computed a few hundred lines above — re-introducing the disabled entry into
<available_tools> even though the actual invocation pool was correctly
filtered.
- The `skills` array feeding SkillEngine/SkillResolver (which
<available_skills> is built from) was never filtered by the agent's
disabled set at all — unlike the parallel installedPlugins/manifest
filtering a few hundred lines above it in the same file.
Both are now filtered by disabledPluginIds. Adds a regression test
asserting the toolManifestMap passed to createOperation excludes a disabled
composio/lobehub-skill identifier.
* 🐛 fix(agent): a disabled skill was still activatable/executable, not just listed
The aiAgent/index.ts fix (previous commit) only stops a disabled skill from
being *listed* in <available_skills> — it doesn't touch actual activation,
because `activateSkill` resolves independently of that list: it queries the
DB directly by name (AgentSkillModel.findByName/findById), or matches
against a separately-constructed builtinSkills array. A model that already
knew a disabled skill's name (from a prior turn, or a guess) could still
activate and run it — this affects both entry points that build a
SkillsExecutionRuntime:
- serverRuntimes/activator.ts (the `lobe-activator` tool's activateSkill
delegation)
- serverRuntimes/skills.ts (the `lobe-skills` tool's own activateSkill,
execScript, etc.)
Both now re-derive the agent's disabled set (AgentModel.getAgentConfigById +
getDisabledPluginIds) and enforce it two ways: builtin skills are filtered
out of the `builtinSkills` array passed to SkillsExecutionRuntime, and DB/
market skills are gated in the injected findByName/findById service methods
(identifier ≠ name for these, so resolution must happen first, then the
resolved skill's `.identifier` is checked against the disabled set).
Adds a new activator.test.ts (none existed) and disabled-skill test cases to
skills.test.ts.
* 🐛 fix(agent): disabled plugin/skill/connector still discoverable in client tools engine
The client legacy runtime's createToolsEngine built its manifest candidate
pool (installed plugins, Composio, LobeHub Skills, connectors, builtin
tools) straight from raw store state, with no disabled-plugin filtering —
unlike the server gateway path (already fixed). Only the "always-on" rule
map excluded disabled entries, but `createEnableChecker`'s
allowExplicitActivation bypass is checked BEFORE rules, so the activator
could still resolve and enable a disabled tool whose manifest was present
in the pool.
Adds `agentSelectors.currentAgentDisabledPlugins` (mirrors
`currentAgentPlugins`) and a new `disabledPluginIds` option on
`createToolsEngine`/`createAgentToolsEngine` that drops disabled
identifiers from the combined manifest pool outright — including builtin
tools, since ordinary user-toggleable builtins (e.g. ones surfaced in the
Skills panel) share the same `agents.plugins` identifier space.
* 🐛 fix(agent): disabled skill still discoverable/activatable on the client legacy runtime
resolveClientSkills built its candidate pool from toolStore.builtinSkills +
toolStore.agentSkills unconditionally — no disabled-plugin filtering — so a
disabled skill still showed up in <available_skills> (auto mode is the
default, so this wasn't limited to an edge case), and remained resolvable
by name via the client activateSkill path regardless.
Threads a new `disabledPluginIds` param through
contextEngineering → resolveClientSkills, computed at the actual call site
(services/chat/index.ts) from `agentConfig.plugins` — the raw (pre-filter)
field, since the existing `plugins` there is already pinned-only. Both the
builtin and DB skill candidate lists are now filtered before being handed
to SkillEngine.
* 🐛 fix(agent): composio_integrations credential list ignores per-agent disabled state
lobe-creds' <composio_integrations> section (client contextEngineering.ts
and server callLlm.ts) listed the user's account-level Composio OAuth
connections with zero reference to the agent's plugins tri-state — a
disabled integration still showed up as "Connected ... use tools directly",
which could make the model claim it can use something it's actually
disabled from calling (the real tool-calling pool already excludes it, so
the model just can't act on the claim — this was a misleading-prompt issue,
not an execution bypass).
Both call sites now drop disabled identifiers from the connected/available
lists:
- Client: `disabledPluginIds` was already threaded into contextEngineering
for the skills fix, reused here — no new plumbing needed.
- Server: callLlm.ts already has `agentId`/`ctx.serverDB` in scope at this
point (same pattern as the adjacent editingAgentModel lookup a few lines
down), so this is a single self-contained AgentModel lookup — no
operation-metadata threading required.
* 🐛 fix(skills): a pinned skill with no pre-fetched content vanished entirely
Pre-existing bug (not introduced by the tri-state work, but found while
investigating a user report of a pinned skill being invisible to the
model). SkillResolver.resolve() force-marked a skill `activated: true`
purely because its identifier was pinned (in enabledPluginIds), regardless
of whether `content` was actually available.
For a ZIP-bundled skill, content is deliberately withheld until
`activateSkill` mounts its bundle (see the comment in
resolveClientSkills/aiAgent's skills build — pre-injecting content that
references an unmounted bundle would break execScript/readReference). So a
pinned bundled skill ended up `activated: true` with `content: undefined`.
SkillContextProvider partitions on `s.activated && s.content` (inject
directly) vs `!s.activated` (list in <available_skills>) — a skill matching
neither bucket disappears from the injected prompt entirely. That's exactly
what happened: pinned + no content = invisible to the model in both
directions, not auto-injected AND not listed for manual activation.
Now only marks `activated: true` when there's real content (from the
operation's own skill data or a step/accumulated activation) to inject;
otherwise it falls through and stays listed as available, restoring
visibility so the model can still discover and activate it. This doesn't
achieve true zero-step auto-injection for bundled skills (that would need
pre-mounting the bundle at operation creation, a separate, larger change)
but fixes the acute "the model doesn't know this skill exists" bug.
* 🐛 fix(agent): plugin/connector switches misread pinned object-shaped entries
PluginAction, LocalPluginItem, and AgentConnectors still checked
`config.plugins.includes(identifier)` against the raw AgentPluginEntry[]
array, which now mixes bare strings with {identifier, mode} objects. A
pinned entry stored as an object never matched, so the switch rendered
unchecked even though the plugin was active. Normalize with
getActivePluginIds, matching the parent AgentPlugin list.
* 🐛 fix(agent): getAgentDetail state.config.plugins broke Tag rendering on tri-state entries
Both the server (agentManagement.ts) and client (AgentManagerRuntime.ts)
getAgentDetail handlers passed the raw AgentPluginEntry[] straight into
state.config.plugins, but GetAgentDetailState declares it as string[]
and GetAgentDetailRender renders `<Tag key={plugin}>{plugin}</Tag>` —
an object-shaped {identifier, mode} entry there breaks the React key and
throws "Objects are not valid as a React child". Normalize to
identifier strings (annotated with mode when not pinned) once, and
reuse the same summary for both the LLM-facing text and the state
payload.
* ✅ test(agent): cover composio_integrations disabled-state filtering (LOBE-11626)
|
||
|
|
046c245ad9 |
✨ feat(chat): add message text selection actions (#16842)
* ✨ feat(chat): add message text selection actions * 🐛 fix(chat): inject selected text context into model prompts * ♻️ refactor(context-engine): split generic context selections injector * 💄 polish(chat): simplify text selection toolbar |
||
|
|
eaf3013694 |
✨ feat(agent): add interactive code context selections (#16681)
* ✨ feat(agent): add code context selections * 🐛 fix: preserve selection context in user messages * ♻️ refactor: reuse XML attribute escaping |