Skip to content

✨ feat(page-agent): execute tools server-side via HeadlessEditor#15023

Merged
arvinxx merged 6 commits into
canaryfrom
feat/page-agent-server-runtime
Jun 7, 2026
Merged

✨ feat(page-agent): execute tools server-side via HeadlessEditor#15023
arvinxx merged 6 commits into
canaryfrom
feat/page-agent-server-runtime

Conversation

@arvinxx

@arvinxx arvinxx commented May 20, 2026

Copy link
Copy Markdown
Member

πŸ’» Change Type

  • ✨ feat
  • ♻️ refactor

πŸ”€ Description of Change

Page-agent tools (initPage / editTitle / getPageContent / modifyNodes / replaceText) used to run inside the renderer's mounted Lexical instance via EditorRuntime. This PR moves the execution path to the server, against a @lobehub/editor/headless instance, with the renderer reduced to a sync target.

Why

  • The renderer-bound path made every tool call dependent on the live editor (editor unmounted β†’ tool failed; user navigated away β†’ silent state drift; tab switch / network blip β†’ broken stream).
  • All other server-executed tools already feed naturally into our OTel gen-ai + agent-signal + documentHistories audit trail. Page-agent was the only blind spot.
  • Silent failures (LLM reports success but Lexical didn't actually change) had no observability β€” the new path detects them at the export boundary.

How it works now

LLM β†’ BuiltinToolsExecutor (server) sees manifest.executors=['server']
   ↓
PageAgentExecutionRuntime (shell) β†’ PageAgentRuntimeService (impl at registration site)
   β”œβ”€ DocumentModel.findById(docId)
   β”œβ”€ createHeadlessEditor() + hydrate from editorData/content
   β”œβ”€ EditorRuntime.setEditor(headless.kernel) + setTitleHandlers
   β”œβ”€ run the 5 page-agent APIs (shared client/server implementation)
   β”œβ”€ headless.export() β†’ silent-failure invariant check
   └─ DocumentService.updateDocument(docId, …, saveSource: 'llm_call')
       └─ writes documents row + appends documentHistories (free audit trail)
   ↓ tool_end event delivers result.state.document{Content,EditorData,Title,Id}
PageAgentExecutor.onAfterCall (renderer)
   β”œβ”€ EditorRuntime.applyServerSnapshot β†’ editor.setDocument('json', editorData, { keepId: true })
   └─ useDocumentStore.applyServerSnapshot β†’ clear dirty + advance lastSaved*

Layout (follows agent-documents / self-iteration convention)

  • packages/builtin-tool-page-agent/src/ExecutionRuntime/index.ts β€” runtime shell + PageAgentRuntimeService contract; package stays free of @lobehub/editor / @lobechat/editor-runtime static imports
  • packages/builtin-tool-page-agent/src/client/executor/index.ts β€” git mv from src/executor/index.ts; gains onAfterCall to sync renderer state
  • src/server/services/toolExecution/serverRuntimes/pageAgent.ts β€” registration site; directly imports EditorRuntime + createHeadlessEditor, implements the 5 service callbacks, computes editorData hash before/after for silent-failure detection
  • packages/editor-runtime/src/EditorRuntime.ts β€” new applyServerSnapshot() that pushes a server-produced snapshot into the live editor without triggering the auto-save loop
  • src/store/document/slices/editor/action.ts β€” matching store action that marks the row clean after a server write
  • src/features/PageEditor/StoreUpdater.tsx β€” removes the now-dead LLM afterMutateHandler wiring (human-edit path untouched)

Package exports dropped ./executor (the slot used to be a renderer-only executor), gained ./executionRuntime. Existing consumer (src/store/tool/slices/builtin/executors/lobe-page-agent.ts) updated to import from ./client.

πŸ§ͺ How to Test

  • Tested locally
  • Added/updated tests

Automated:

  • packages/builtin-tool-page-agent: 24/25 tests pass (1 pre-existing baseline failure unrelated to this PR; see commit history on client/executor/index.test.ts).
  • src/server/services/toolExecution/** + src/store/document/slices/editor/**: 230/230 pass.
  • Project-wide `bun run type-check` is clean for all changed files (pre-existing drizzle-orm dual-copy noise unaffected).

Manual:

  1. Open a page editor in a topic (scope=page).
  2. Ask the LLM to edit the document (e.g. `add a paragraph about X`, `change the title to Y`, `replace 'foo' with 'bar'`).
  3. Confirm:
    • The DB `documents.editorData` / `content` updates, and a `documentHistories` row gets appended with `saveSource='llm_call'`.
    • The live editor reflects the change without a refresh and the dirty indicator stays clean.
    • Closing & reopening the page yields the same content (no client-only state).

πŸ“ Additional Information

Invariant violation is surfaced as a console.warn plus a state.invariantViolation = { apiName, kind, detail } field on the tool result. `kind` is one of:

  • silent-no-op β€” handler reported a successful mutation but the editorData hash did not change (e.g. `modifyNodes` operations all pointed at non-existent node IDs).
  • unexpected-mutation β€” editor state changed even though the handler reported no successful change.

Stability boundary: `editorChanged` is computed by hashing the exported `editorData` before and after the handler runs; `editTitle` is exempt because the title lives outside `editorData`.

Not in this PR: surfacing invariant violations in the UI, agent-signal source events on violation, and richer trace headers (these belong to the broader tracing-foundation work this PR unblocks).

πŸ€– Generated with Claude Code

@vercel

vercel Bot commented May 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lobehub Ready Ready Preview, Comment Jun 6, 2026 6:30pm

Request Review

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label May 20, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @arvinxx, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@dosubot dosubot Bot added feature:editor something links lobehub editor / rich content text / markdown render feature:page feature:tool Tool calling and function execution labels May 20, 2026
@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 7.11974% with 287 lines in your changes missing coverage. Please review.
βœ… Project coverage is 70.63%. Comparing base (573cc5b) to head (4a94293).
⚠️ Report is 2 commits behind head on canary.

Additional details and impacted files
@@            Coverage Diff             @@
##           canary   #15023      +/-   ##
==========================================
- Coverage   70.69%   70.63%   -0.07%     
==========================================
  Files        3277     3278       +1     
  Lines      323408   323716     +308     
  Branches    29521    29521              
==========================================
+ Hits       228641   228660      +19     
- Misses      94584    94873     +289     
  Partials      183      183              
Flag Coverage Ξ”
app 61.32% <7.11%> (-0.08%) ⬇️
database 92.47% <ΓΈ> (ΓΈ)
packages/agent-manager-runtime 49.69% <ΓΈ> (ΓΈ)
packages/agent-runtime 81.06% <ΓΈ> (ΓΈ)
packages/builtin-tool-lobe-agent 18.52% <ΓΈ> (ΓΈ)
packages/context-engine 84.19% <ΓΈ> (ΓΈ)
packages/conversation-flow 91.29% <ΓΈ> (ΓΈ)
packages/device-gateway-client 90.18% <ΓΈ> (ΓΈ)
packages/eval-dataset-parser 95.15% <ΓΈ> (ΓΈ)
packages/eval-rubric 76.11% <ΓΈ> (ΓΈ)
packages/fetch-sse 85.57% <ΓΈ> (ΓΈ)
packages/file-loaders 87.89% <ΓΈ> (ΓΈ)
packages/memory-user-memory 74.99% <ΓΈ> (ΓΈ)
packages/model-bank 99.99% <ΓΈ> (ΓΈ)
packages/model-runtime 84.22% <ΓΈ> (ΓΈ)
packages/prompts 72.51% <ΓΈ> (ΓΈ)
packages/python-interpreter 92.90% <ΓΈ> (ΓΈ)
packages/ssrf-safe-fetch 0.00% <ΓΈ> (ΓΈ)
packages/types 35.38% <ΓΈ> (ΓΈ)
packages/utils 84.98% <ΓΈ> (ΓΈ)
packages/web-crawler 88.08% <ΓΈ> (ΓΈ)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Ξ”
Store 68.35% <6.89%> (-0.06%) ⬇️
Services 54.77% <ΓΈ> (ΓΈ)
Server 71.78% <7.14%> (-0.23%) ⬇️
Libs 54.49% <ΓΈ> (ΓΈ)
Utils 81.71% <ΓΈ> (ΓΈ)
πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

arvinxx and others added 4 commits June 7, 2026 00:50
Page-agent tools (initPage / editTitle / getPageContent / modifyNodes /
replaceText) now run on the server against a `@lobehub/editor/headless`
instance and persist through `DocumentService.updateDocument`, instead
of executing inside the renderer's Lexical instance. The renderer
applies the resulting snapshot via the builtin-tool `onAfterCall` hook,
so the document store stays in sync without an extra fetch.

This makes page-agent execution independent of the client lifecycle
(editor unmount, tab switch, network blip), gives us full server-side
tracing for free (OTel gen-ai + agent-signal + documentHistories), and
exposes a `silent-no-op` / `unexpected-mutation` invariant when the
exported editorData hash diverges from what the handler reported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ffecting bundle

EditorRuntime statically imported LITEXML_*_COMMAND from @lobehub/editor,
which pulls ReactSlashPlugin and crashes Node (`document is not defined`)
in any server-side test that transitively touched the runtime. The same
import also dispatched the wrong command identity on HeadlessEditor's
kernel β€” pnpm resolves @lobehub/editor to a different module copy than
the headless bundle, so dispatchCommand would silently no-op server-side.

Introduce a LiteXMLAdapter strategy: renderer wires command dispatch
against the live editor; server wires HeadlessEditor.applyLiteXMLBatch
/ applyLiteXML so the correct headless-bundle symbols are used.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…mount

The main commit dropped `setBeforeMutateHandler`/`setAfterMutateHandler`
under the assumption that page-agent tools always execute server-side.
But the chat-store path (`invokeBuiltinTool` β†’ `PageAgentExecutor.modifyNodes`
β†’ `EditorRuntime.modifyNodes`) still routes through the client-bound
runtime whenever the LLM dispatcher is the chat slice β€” it does not
consult `manifest.executors`. Without the handlers, that path mutates
the live editor but skips both `documentHistoryQueueService.enqueueEditorSnapshot`
(loses undo baseline) and `commitEditorMutation(saveSource: 'llm_call')`
(row never persists).

Re-wire both handlers. Server-runtime path is unaffected: it instantiates
its own `EditorRuntime` against `HeadlessEditor` and never sees the
client's StoreUpdater wiring, so the two paths can coexist without
double-writing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r gets adapter for free

Renderer call sites shouldn't have to opt in to the obvious default
(dispatch LITEXML_*_COMMAND on the live editor). Split the package into
two entries:

- `@lobechat/editor-runtime` β€” renderer entry; constructor auto-wires
  the LiteXML adapter from `@lobehub/editor`. Static-importing this
  from Node still crashes (ReactSlashPlugin), so it's the right shape
  for the browser only.
- `@lobechat/editor-runtime/server` β€” server-safe entry; exports the
  bare class without touching `@lobehub/editor`. Callers (currently
  only the page-agent server runtime) supply their own HeadlessEditor-
  backed adapter.

Drops the renderer-side setLiteXMLAdapter patch and a stale comment
block in StoreUpdater.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
arvinxx and others added 2 commits June 7, 2026 02:02
`@lobehub/editor` 4.16.1 ships the LiteXML command identities through the
side-effect-free `@lobehub/editor/litexml-commands` subpath, so a single command
object is shared across the browser and node bundles and can be imported in Node
without pulling the DOM-dependent editor bundle.

`EditorRuntime` now imports `LITEXML_MODIFY_COMMAND` / `LITEXML_APPLY_COMMAND`
from that subpath and dispatches them straight onto the editor kernel. This
removes the `LiteXMLAdapter` strategy object (`setLiteXMLAdapter` /
`getLiteXMLAdapter`) β€” a leaky abstraction whose only purpose was to keep the
crash-on-Node command import out of the shared base.

- editor-runtime: dispatch `LITEXML_*_COMMAND` directly; delete the adapter
  interface, field, setter and runtime-throw guard.
- Collapse the client/server entry split (its sole reason β€” isolating the
  DOM-crashing import β€” is gone); both entries now re-export the isomorphic base.
- pageAgent server runtime: drop the HeadlessEditor-backed adapter wiring.
- Bump `@lobehub/editor` to ^4.16.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that `EditorRuntime` is isomorphic (LiteXML commands come from the DOM-free
`@lobehub/editor/litexml-commands` subpath), the `./server` entry is byte-for-byte
identical to the root `.` entry. Remove it and point the only consumer
(pageAgent server runtime) at the root entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@arvinxx arvinxx merged commit 20cea3a into canary Jun 7, 2026
33 of 35 checks passed
@arvinxx arvinxx deleted the feat/page-agent-server-runtime branch June 7, 2026 14:33
arvinxx added a commit that referenced this pull request Jun 10, 2026
# πŸš€ LobeHub Release (20260610)

**Release Date:** June 10, 2026  
**Since v2.2.2:** 131 merged PRs Β· 13 contributors

> This weekly release strengthens agent collaboration across cloud,
desktop, CLI, and workspace flows, with steadier runtime behavior and a
broader foundation for workspace-scoped data.

---

## ✨ Highlights

- **Agent execution across devices** β€” Unifies per-device working
directories, project skill discovery, and sub-agent suspend/resume
behavior across server, QStash, and device RPC flows. (#15543, #15566,
#15481, #15620, #15591)
- **Connector and sandbox platform** β€” Expands connector permissions,
custom OAuth MCP connector onboarding, sandbox provider support, and
user-uploaded file sync into cloud sandbox runs. (#15463, #15546,
#15184, #15550)
- **Desktop and CLI reliability** β€” Fixes desktop cold-start,
auto-update, Windows build, CLI skill discovery, and `lh connect` agent
dispatch paths. (#15547, #15525, #15527, #15562, #15632, #15634)
- **Pages and sharing** β€” Refreshes topic sharing, improves Page Editor
layout behavior, and routes Page Agent tool execution through the
server-side editor path. (#15581, #15556, #15588, #15023, #15610)
- **Model availability and provider updates** β€” Adds user-scoped LobeHub
model availability, Claude Fable 5, Qwen thinking preservation, and
MiniMax M3 updates. (#15590, #15639, #13494, #15376)

---

## πŸ—οΈ Core Product & Architecture

### Agent Runtime & Heterogeneous Agents

- Improves sub-agent lifecycle handling, including async suspend/resume,
queue-mode QStash resume delivery, and blocking nested sub-agent calls.
(#15481, #15620, #15575)
- Stabilizes heterogeneous agent ingestion and streaming with raw stream
dumps, per-turn usage, image forwarding on regenerate, and
duplicate-text fixes. (#15602, #15577, #15592, #15585)
- Adds execution-device and working-directory controls across device
RPC, legacy defaults, and remote-spawned Claude Code sessions. (#15543,
#15566, #15591, #15572)
- Improves runtime diagnostics and compatibility, including Gemini
multimodal output capture, abort stream semantics, and trace quality
analysis. (#15535, #13677, #15508)

---

## πŸ“± Platforms, Integrations & UX

### Connectors, Sandbox & Tools

- Ships API-level connector tool permissions, custom OAuth MCP connector
onboarding, and connector-first runtime execution. (#15463, #15546)
- Adds sandbox provider support, cloud sandbox file sync, and safer
external URL file input handling with SSRF validation. (#15184, #15550,
#12657)
- Improves tool visibility and execution with pinned app-fixed tools,
ANSI output rendering, gateway-tunneled MCP calls, and automatic
headless tool runs. (#15509, #15516, #15469, #15492)

### Desktop, CLI & Web UX

- Restores desktop startup and reload behavior, preserves IPC error
causes, and keeps the tab bar new-tab action visible across routes.
(#15547, #15597, #15638)
- Fixes desktop update and build stability for browser quit guards,
macOS update signing, and Windows Visual Studio detection. (#15525,
#15527, #15562)
- Shows the plan-limit upgrade UI on desktop builds. (#15628)
- Adds the Agent Run delivery checker and fixes CLI device dispatch plus
skill list/search output. (#15489, #15634, #15632)
- Refreshes onboarding, auth source preservation, topic UI states,
referral/Fable campaign copy, and chat-input control bar behavior.
(#15629, #15544, #15573, #15614, #15616, #15617, #15622, #15643)

---

## πŸ”’ Security, Reliability & Rollout Notes

- External URL file input now includes SSRF validation for safer Google
file handling. (#12657)
- Database workspace-scope migrations are part of this release;
self-hosted operators should run the normal migration path before
serving the updated app. (#15446, #15465, #15468, #15472)
- The release branch was re-cut from `canary` and includes the latest
`main` release-version commit so `v2.2.2` is the verified compare base.

---

## πŸ‘₯ Contributors

@ONLY-yours, @sxjeru, @hardy-one, @xujingli, @hezhijie0327, @Coooolfan,
@arvinxx, @tjx666, @Innei, @rivertwilight, @rdmclin2, @cy948,
@AmAzing129

**Full Changelog**:
v2.2.2...release/weekly-20260610-recut-3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:editor something links lobehub editor / rich content text / markdown render feature:page feature:tool Tool calling and function execution size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant