Commit Graph

190 Commits

Author SHA1 Message Date
Arvin Xu 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>
2026-09-12 23:36:03 +08:00
Innei 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.
2026-09-12 12:35:36 +08:00
Arvin Xu 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>
2026-09-12 01:56:19 +08:00
LiJian 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>
2026-09-09 17:42:09 +08:00
Rdmclin2 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>
2026-09-09 17:25:05 +08:00
Rdmclin2 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>
2026-09-09 11:02:54 +08:00
Innei 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
2026-09-09 02:45:25 +08:00
Arvin Xu 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>
2026-09-07 00:24:33 +08:00
Arvin Xu 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'.
2026-09-04 09:57:30 +08:00
Neko 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>
2026-09-04 09:50:55 +08:00
Arvin Xu 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>
2026-09-03 15:56:58 +08:00
Arvin Xu 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>
2026-09-03 09:27:09 +08:00
YuTengjing e863261cb7 🐛 fix(desktop): recognize AVIF files as images (#18867) 2026-08-30 16:35:49 +08:00
Arvin Xu 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>
2026-08-29 00:41:12 +08:00
YuTengjing 774e04c44b 🐛 fix: support media refs for tool result images (#18743) 2026-08-28 19:45:12 +08:00
Arvin Xu 1dae711792 🐛 fix(tools): recover stale scoped tool calls (#18664)
Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
2026-08-25 12:35:34 +08:00
Arvin Xu 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>
2026-08-24 13:35:25 +08:00
Rylan Cai 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
2026-08-24 11:38:23 +08:00
Arvin Xu e41b6cb0de ✨ feat(goal): gate work tasks with acceptance (#18623) 2026-08-23 23:42:06 +08:00
Rylan Cai 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
2026-08-21 17:00:29 +08:00
Arvin Xu 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
2026-08-17 16:07:56 +08:00
Arvin Xu 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>
2026-08-15 12:15:19 +08:00
Tsuki 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
2026-08-09 19:42:32 -07:00
Arvin Xu 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>
2026-08-10 05:50:39 +08:00
Tsuki 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
2026-08-09 12:52:21 -07:00
Rylan Cai 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
2026-08-10 00:30:39 +08:00
YuTengjing cc064ee9b1 🐛 fix(context-engine): use absolute dates in agent documents index (#17981) 2026-08-06 23:26:41 +08:00
YuTengjing 5b348e814c 🐛 fix(context-engine): carry dynamic activation docs to the LLM payload once (#17972) 2026-08-06 23:13:33 +08:00
Arvin Xu 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
2026-08-06 01:33:44 +08:00
Hardy 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.
2026-08-06 01:08:02 +08:00
YuTengjing a27dfaeda1 ✨ feat: support audio in multimodal understanding (#17904) 2026-08-05 11:08:46 +08:00
Arvin Xu 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>
2026-08-04 11:26:24 +08:00
sxjeru ba69b32909 🐛 fix(model-runtime): support advanced reasoning parameters for Cerebras and Groq (#16469)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-08-02 14:33:49 +08:00
YuTengjing b6a8fcf0dc 🐛 fix: filter failed assistant placeholders and harden Claude prefill guard (#17737) 2026-07-30 21:31:10 +08:00
Rylan Cai 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
2026-07-29 16:44:25 +08:00
YuTengjing b36a003ac5 🐛 fix(model-runtime): sanitize replayed thinking history for anthropic-compatible providers (#17629) 2026-07-26 21:08:55 +08:00
AmAzing- 3384fe491e ✨ feat: add workspace topic comments (#17537) 2026-07-25 18:21:41 +08:00
YuTengjing 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
2026-07-24 21:57:22 +08:00
LiJian 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>
2026-07-24 11:49:44 +08:00
YuTengjing 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
2026-07-23 10:40:17 +08:00
LiJian 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>
2026-07-16 14:44:21 +08:00
AmAzing- f945c00d50 ♻️ refactor(lint): clean up eslint-disable directives (#17206) 2026-07-15 05:28:23 +08:00
Arvin Xu 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>
2026-07-13 10:52:55 +08:00
Arvin Xu 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
2026-07-12 16:09:13 +08:00
Arvin Xu 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 `![](url)` 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>
2026-07-11 23:42:59 +08:00
Rylan Cai a5b1e52641 🐛 fix: runtime unresolved tool calls (#17044) 2026-07-11 21:30:20 +08:00
Rylan Cai 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
2026-07-10 15:01:35 +08:00
LiJian 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)

df47cc6a0e shipped with zero test coverage — the <composio_integrations>
disabled-filtering logic was duplicated inline inside two giant, hard-to-test
functions (client contextEngineering.ts, server callLlm.ts), each reaching
into a live store/DB. Extracts the actual filtering (excludeDisabledComposioServices,
resolveAvailableComposioServices) into packages/builtin-tool-creds/src/helpers.ts
as pure functions shared by both call sites, and adds unit tests — including a
regression case mirroring the real bug (a disabled service resurfacing in the
"available to connect" list).
2026-07-09 18:31:26 +08:00
Arvin Xu 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
2026-07-08 01:45:20 +08:00
Innei 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
2026-07-06 14:50:49 +08:00