Skip to content

feat(plugins): full-stack plugin APIs and bookmarks - #5647

Merged
WillemJiang merged 6 commits into
bytedance:mainfrom
AnnaSuSu:feat/full-stack-plugin-contributions
Sep 22, 2026
Merged

WillemJiang merged 6 commits into
bytedance:mainfrom
AnnaSuSu:feat/full-stack-plugin-contributions

Conversation

@AnnaSuSu

@AnnaSuSu AnnaSuSu commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Related: #5539 and #5510. This is the first implementation slice, not a claim to complete either RFC.

Why

Python extensions can add backend behavior, but a full feature with its own UI still needs core frontend changes. This PR lets a trusted deployment-installed package provide its own browser page, conversation actions, backend operations and model tools. The standalone bookmark package exercises that path end to end.

What changed

  • Add registry.plugin(PluginContribution(...)) to extension-api 0.2.2, with source attribution, validation and rollback. Browser modules, async actions and model tools share one plugin namespace.
  • Serve authenticated descriptors, content-addressed ES modules and declared actions under /api/plugins. Deployment configuration/CLI owns installation and activation; no online configuration write or override store is added.
  • Load plugin pages and conversation actions at runtime. Capability Center shows read-only plugin metadata/status. Plugins can register standalone workspace pages and optional sidebar links, without replacing native notification or export behavior.
  • Isolate malformed conversation actions per plugin, preserve ordinary and healthy plugin tools on host-name collisions, and keep host UI text in the typed English/Chinese locale dictionaries.
  • Resolve bookmark navigation through the authenticated host thread API and existing Agent routing rules, including older bookmarks. Cancel navigation after surface cleanup; contain rejected async action factories and availability callbacks while keeping the synchronous contract.
  • Pin durable batch workers to the Gateway app's extension snapshot for both tools and execution; recovered items use the new worker generation without serializing plugin objects into task records.
  • Provision Python/backend dependencies in the E2E workflow and trigger it for the bookmark package and its host contract paths.
  • Add examples/deerflow-extension-bookmarks: save a visible answer, manage it under My bookmarks, and retrieve personal bookmarks with a read-only model tool. The package owns its SQLite data and browser implementation; the host has no bookmark-specific page or business router.

This slice supports pages and conversation actions. Composer selection, right-side panels, hot unloading, a marketplace and built-in runtime configuration editors remain out of scope. Adding this host API requires a DeerFlow update; subsequent compatible plugin UI packages do not require rebuilding the host frontend. Server package/config changes require restart and browsers update on manual refresh.

Experimental browser transport: BrowserModule(code=...) is an MVP single-file transport, not the stable long-term packaging contract. The plugin guide describes an additive, versioned manifest/static-resource transport, safe asset serving, private immutable caching and authenticated relative-chunk loading. Those packaged-asset APIs are follow-up design, not implemented in this slice; existing inline packages should remain supported.

Trust boundary: browser modules run as operator-trusted same-origin code. Shadow DOM scopes CSS, not privileges. Sandboxed iframe isolation and a broader compatibility contract remain design discussions in #5510/#5539. Bookmark persistence is single-host and stores a user-submitted answer copy; tests do not claim production identity or live LLM verification.

Surface area

  • Frontend UI — extension catalog, generic page/navigation/action slots
  • Backend API — authenticated discovery, asset serving and action invocation
  • Agents / LangGraph — model tools join normal tool assembly and retain the parent extension snapshot during delegation; bookmark results remain tool data, not system instructions
  • Sandbox
  • Skills
  • Dependencies — extension-api additive release to 0.2.2; jsonschema is made a direct harness dependency for tool input validation (already present in the backend dependency graph). Parallel PR feat(skills): add guarded evolution host APIs #5645 also extends extension-api 0.2.2; these features are independent, but exports/version metadata will need alignment when rebasing
  • Default behavior change — adds the read-only Extensions tab and descriptor discovery; no plugin is installed or enabled by default
  • Docs / tests / CI only

Screenshots / Recording

Screenshots use synthetic data.

Capability Center: discover installed extensions Standalone plugin page: My bookmarks
Extension directory My bookmarks

Validation

  • Backend make format and make lint: passed. Focused plugin tool contracts: 7 passed, including collisions with config, built-in, MCP and ACP tools. The four assembly cases failed before the fix; strict plugin-vs-plugin duplicate rejection remains covered. Blocking I/O suite: 149 passed.
  • Full backend make test: 18,029 passed / 15 failed / 206 skipped. The failing node IDs exactly match the recorded clean-main full-suite baseline (29d28573, 18,008 passed / 15 failed / 205 skipped). All 15 failing nodes were also rerun on that clean main checkout and reproduced, with 0 new failures. Public test URLs hit the local DNS/SSRF address guard; these are not plugin failures.
  • Frontend validation at f78c2061 (frontend unchanged in the batch follow-up): pnpm format, pnpm check (lint + TypeScript): passed. Full unit suite: 1,934 passed. The action component regressions cover callback exceptions, malformed groups/actions, availability failures, duplicate IDs, snapshot getters and icon fallback; ten cases failed before the isolation fix. Module-loader coverage includes authenticated requests and configured backend path prefixes.
  • Production build: SKIP_ENV_VALIDATION=1 DEER_FLOW_AUTH_DISABLED=1 pnpm exec next build --webpack passed.
  • Production Chromium E2E: bookmark-plugin.spec.ts, capability-center.spec.ts, settings-notification.spec.ts: 21 passed. This covers the real Python bookmark router, SQLite persistence, scripted ToolNode lookup, owner isolation, navigation, reload and deletion; three deliberately broken plugin action groups coexist with a working conversation/action without browser page errors; bookmarks saved from the default conversation, custom-Agent toolbar and custom-Agent sidebar reopen the correct route and send the next request to the original Agent; extension host labels and search are checked in English and Chinese. Authentication and other backend APIs are fixture-controlled. The entire E2E suite was not run; no live model/provider verification is claimed.
  • Batch snapshot follow-up: 140 focused backend tests passed, including replacements before assembly/execution, explicit empty snapshots, recovery under a new worker and Gateway lifespan injection. The tests exercise production tool assembly and executor selection with fixture persistence and a scripted graph. The existing executor fixture also passes in isolation after removing its import-order dependency.
  • Initial standalone package validation: isolated CLI install/list/disable/enable/remove, installed wheel/assets/SQLite, and normal get_available_tools → ToolNode owner/group checks passed. E2E workflow setup YAML and all 5 uv/action pin contract tests passed.
  • Agent guidance check: 31 guidance files, 0 errors and 2 advisory size warnings.

AI assistance

Tool(s) used: Codex.

How you used it: Developed the host interfaces and bookmark example, refined the scope from local prototypes, wrote and ran tests, and reviewed the resulting diff with AI assistance.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

@AnnaSuSu
AnnaSuSu requested review from Vanzeren and zhfeng and a lite review from Copilot September 21, 2026 07:04
@github-actions github-actions Bot added area:backend Gateway / runtime / core backend under backend/ area:deps Dependency manifests / lockfiles area:docs Documentation and Markdown only area:frontend Next.js frontend under frontend/ needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/XL PR changes 700+ lines labels Sep 21, 2026
@github-actions github-actions Bot added the area:ci GitHub Actions, CI config, repo tooling label Sep 21, 2026

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Five unresolved moderate findings affect plugin activation, module loading and validation, route preservation, and action isolation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds full-stack plugin APIs, runtime frontend integration, and a standalone SQLite bookmarks extension.

Changes:

  • Adds plugin registration, authenticated assets/actions, and model-tool assembly.
  • Adds frontend discovery, navigation, pages, and conversation actions.
  • Adds the bookmarks example, documentation, fixtures, and tests.
File Reviewed change
README.md Documents full-stack plugins.
frontend/​tests/​unit/​core/​extensions/​surfaces.dom.test.ts Tests surface lifecycle.
frontend/​tests/​unit/​core/​extensions/​services.test.ts Tests plugin services.
frontend/​tests/​unit/​core/​extensions/​pages.test.ts Tests plugin page routing.
frontend/​tests/​e2e/​utils/​mock-api.ts Mocks plugin discovery.
frontend/​tests/​e2e/​capability-center.spec.ts Tests capability-center behavior.
frontend/​tests/​e2e/​bookmark-plugin.spec.ts Covers the bookmark workflow.
frontend/​src/​core/​extensions/​surfaces.ts Mounts and cleans up surfaces.
frontend/​src/​core/​extensions/​services.ts Provides plugin services.
frontend/​src/​core/​extensions/​registry.ts Loads and validates browser modules; moderate finding (3 votes) remains for configured backend URL and authenticated loading.
frontend/​src/​core/​extensions/​pages.ts Builds plugin page routes.
frontend/​src/​core/​extensions/​hooks.ts Provides extension queries.
frontend/​src/​core/​extensions/​contracts.ts Defines frontend contracts.
frontend/​src/​core/​extensions/​api.ts Fetches plugin descriptors.
frontend/​src/​components/​workspace/​workspace-sidebar.tsx Adds plugin navigation.
frontend/​src/​components/​workspace/​thread-extension-actions.tsx Adds thread actions.
frontend/​src/​components/​workspace/​recent-chat-list.tsx Adds sidebar actions.
frontend/​src/​components/​workspace/​plugin-surfaces.tsx Renders plugin surfaces.
frontend/​src/​components/​workspace/​plugin-page.tsx Hosts plugin pages.
frontend/​src/​components/​workspace/​plugin-navigation.tsx Renders plugin links.
frontend/​src/​components/​workspace/​conversation-extension-actions.tsx Renders conversation actions; moderate finding (1 vote) remains for plugin error isolation.
frontend/​src/​components/​workspace/​chats/​chat-page.tsx Adds chat toolbar actions.
frontend/​src/​components/​workspace/​capabilities/​extension-gallery.tsx Displays extension metadata.
frontend/​src/​components/​workspace/​capabilities/​capability-center.tsx Adds the Extensions tab.
frontend/​src/​app/​workspace/​workspace-content.tsx Bootstraps extensions.
frontend/​src/​app/​workspace/​extensions/​[namespace]/​[surface_id]/​page.tsx Adds extension page routing.
frontend/​src/​app/​workspace/​agents/​[agent_name]/​chats/​[thread_id]/​page.tsx Adds actions to custom-agent chats.
frontend/​AGENTS.md Documents frontend plugin architecture.
examples/​deerflow-extension-bookmarks/​THIRD_PARTY_NOTICES.md Provides attribution and licensing.
examples/​deerflow-extension-bookmarks/​README.md Documents bookmark deployment and use.
examples/​deerflow-extension-bookmarks/​pyproject.toml Defines example package metadata.
examples/​deerflow-extension-bookmarks/​deerflow_extension_bookmarks/​client.mjs Implements bookmark UI/actions; moderate finding (1 vote) remains for preserving custom-agent routes.
examples/​deerflow-extension-bookmarks/​deerflow_extension_bookmarks/​__init__.py Implements bookmark backend and tool.
docs/​full-stack-plugins.md Documents plugin contracts and lifecycle.
backend/​uv.lock Updates locked dependencies.
backend/​tests/​test_task_tool_core_logic.py Tests delegated extension snapshots.
backend/​tests/​test_plugin_tools.py Tests model-tool dispatch and policy.
backend/​tests/​test_plugin_contributions.py Tests plugin registration and behavior.
backend/​tests/​test_extension_api_surface.py Tests public API exports.
backend/​tests/​test_extension_api_contracts.py Tests API version contracts.
backend/​tests/​test_bookmark_plugin.py Tests bookmark persistence and isolation.
backend/​packages/​harness/​pyproject.toml Pins extension API and schema dependencies.
backend/​packages/​harness/​deerflow/​tools/​tools.py Adds plugin tools to assembly.
backend/​packages/​harness/​deerflow/​tools/​builtins/​task_tool.py Propagates extension snapshots.
backend/​packages/​harness/​deerflow/​extensions/​registry.py Registers plugins; moderate finding (1 vote) remains for validating module identifiers.
backend/​packages/​harness/​deerflow/​extensions/​plugin_tools.py Builds guarded model tools.
backend/​packages/​harness/​deerflow/​extensions/​AGENTS.md Documents backend plugin behavior.
backend/​packages/​harness/​deerflow/​config/​plugin_settings.py Validates plugin settings.
backend/​packages/​extension-api/​pyproject.toml Bumps the extension API version.
backend/​packages/​extension-api/​deerflow_extension_api/​settings.py Defines settings contracts.
backend/​packages/​extension-api/​deerflow_extension_api/​plugins.py Defines plugin contribution types.
backend/​packages/​extension-api/​deerflow_extension_api/​contracts.py Adds the registry plugin protocol.
backend/​packages/​extension-api/​deerflow_extension_api/​__init__.py Exports new API symbols.
backend/​extension_test_fixtures/​bookmark_plugin_gateway.py Provides the bookmark Gateway fixture.
backend/​app/​gateway/​routers/​plugins.py Adds plugin endpoints; moderate finding (1 vote) remains for rechecking activation before serving assets.
backend/​app/​gateway/​app.py Mounts the plugin router.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread frontend/src/core/extensions/registry.ts Outdated
@zhfeng

zhfeng commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Thanks @AnnaSuSu - I will take a look.

@willem-bd willem-bd 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.

Reviewed the head (80702d5). The overall shape is solid: the registry contribution is validated before any bucket mutation with positional rollback, the browser entry path is pinned to the content-addressed same-origin asset route, backend actions/tools are auth-gated with size and timeout bounds, and the bookmark example keeps owner predicates on every SQLite statement (verified by executing the head's plugin suite in a clean worktree: 59/59 focused tests pass; the only 2 local failures are stale dist-metadata artifacts of my shared venv, not PR code). Three suggestions below: (1) a plugin tool name collision currently fails the entire agent tool assembly rather than degrading per-source like every other failure mode in get_available_tools; (2) conversationActions output is unvalidated and runs unguarded during render, so one malformed plugin can take down the whole workspace page even though surfaces are rigorously contained; (3) ~40 new user-facing strings are hardcoded bilingual ternaries instead of going through the i18n locale dictionaries. The known split-origin dynamic-import concern on registry.ts is already covered by an earlier comment, so I won't duplicate it.

from deerflow.extensions.plugin_tools import build_plugin_tools

ordinary_tools = loaded_tools + builtin_tools + mcp_tools + acp_tools
plugin_tools = build_plugin_tools(extensions if extensions is not None else get_agent_build_extensions(), groups=groups, reserved_names={t.name for t in ordinary_tools})

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.

A name collision here raises ValueError out of get_available_tools, which fails the entire tool assembly — every lead and subagent build in the process, not just the offending plugin's tool. Every other failure source in this function degrades per-source (config tool resolution, MCP cache, and ACP setup are each wrapped in try/except that log and continue), and registration can't check host tool names, so this is only reachable when an ordinary/config/MCP tool happens to be named exactly like a generated ext_<ns>_<name>_<digest> — an external MCP server exposing a colliding name would break all chat builds until an operator intervenes. Consider catching the collision at this call site and degrading to log-and-drop of the plugin tool, keeping the strict raise inside build_plugin_tools for plugin-vs-plugin duplicates.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 413a68f. I reused the existing ordinary-first deduplication in get_available_tools: plugin tools are assembled with their internal duplicate checks, then the shared deduplication logs and drops only names already provided by ordinary tools. This preserves unrelated plugin tools, rather than dropping the whole plugin-tool batch on a caught exception. The strict build_plugin_tools duplicate/reserved-name checks remain available to direct callers.

The regression runs the real assembly against collisions from all four ordinary sources (config, built-in, MCP and ACP), checks that the original tool still executes, and verifies that a healthy plugin survives and plugin-vs-plugin duplicates still raise. All four cases failed before the fix and pass now. Full backend testing has no new failing node IDs; the 15 local DNS/SSRF failures also reproduce on clean main.


return activeFrontendExtensions(entries).map(
({ contribution, extension }) => {
const group = extension.conversationActions?.(t, locale);

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.

loadFrontendExtensions deeply validates surfaces (ids, titles, navigation, mount) so a malformed module is contained to its own error state, but conversationActions is only checked to be a function — the returned group shape is unvalidated, and it is executed here during render of every chat header and every sidebar conversation row. A plugin whose conversationActions() throws, or returns { actions: undefined }, makes group.actions.filter(...) throw; the render error then hits the route-level error boundary and takes down the whole workspace page, including conversations unrelated to that plugin. Consider validating the group shape at load time in core/extensions/registry.ts (mirroring the surface checks) or wrapping this per-contribution block in try/catch and skipping the plugin.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 413a68f. Both toolbar and sidebar now use a shared per-contribution resolver that guards the locale-dependent factory and availability callbacks, validates group/action fields and unique IDs, and passes copied, validated values into JSX. Exceptions or malformed values omit only that plugin's group. Unknown icon names also use an own-property lookup so names such as __proto__ cannot supply an invalid React component. Execution failures keep the existing toast path.

The DOM regressions cover throwing factories, malformed groups/actions, unavailable or asynchronous availability, duplicate IDs, getter snapshots and icon fallback while retaining a healthy sibling. Ten cases failed before the fix. Two production-browser cases load a throwing/malformed module alongside a working module and successfully execute the healthy action without losing the conversation. Full frontend unit tests: 1,924 passed; targeted production browser tests: 18 passed.

<BlocksIcon className="size-4" />
{t.capabilities.plugins}
{locale.startsWith("zh")
? "工具与集成"

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.

This swaps t.capabilities.plugins for inline locale.startsWith("zh") ? ... : ... strings, and the new extension components (plugin-page, plugin-navigation, extension-gallery, conversation-extension-actions) add ~40 more user-facing strings the same way. The rest of the workspace routes copy through core/i18n/locales/en-US.ts/zh-CN.ts and the Translations type; hardcoding bypasses the dictionary and any future locale tooling. Also note the search box above still uses t.capabilities.searchPlugins for the extensions tab, so the placeholder reads "Search plugins" while browsing extensions. Suggest adding these keys to both locale files for consistency.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 413a68f. Host copy across all six extension UI components now comes from the typed en-US/zh-CN dictionaries, including the surface-error message. The Extensions tab has its own search placeholder and accessible label; Tools & integrations and Skills retain their respective labels. Plugin-authored content stays owned by the independent plugin package.

Production-browser regressions verify both English and Chinese extension labels, search, empty state and unavailable-page navigation. Format, lint, type checks, all 1,924 frontend unit tests and all 18 targeted browser tests pass.

@zhfeng

zhfeng commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Thanks for putting together this first implementation slice. The runtime discovery and end-to-end plugin path are valuable, but I want to call out one important difference from RFC #5510: the RFC proposed a manifest plus packaged static assets (ui_manifest.json and static/dist/...), whereas this PR embeds a single self-contained ES module as BrowserModule.code.

The inline-module approach is a reasonable way to validate the host contract, but it is too restrictive as the long-term asset model. Requiring every browser module to be self-contained means a non-trivial plugin cannot naturally use code splitting, lazy-loaded chunks, CSS files, images, fonts, WASM, source maps, or assets referenced through import.meta.url. It can also force large bundles to be loaded and retained as one Python string, while the current no-store response prevents normal immutable browser/CDN caching even though the URL is content-addressed. This will become a practical limitation for complex dashboards, editors, visualizations, and domain-specific applications.

I suggest treating BrowserModule(code=...) as an MVP transport rather than the final plugin packaging contract, and preserving an explicit path toward packaged static resources. For example, a plugin contribution could declare a validated manifest and an asset root, while the Gateway serves only files under a namespace-scoped, traversal-safe, content-addressed route such as /api/plugins/{namespace}/assets/{revision}/{path}. The manifest should identify the entry module and allowed assets, and the host should provide correct MIME types plus immutable caching for revisioned URLs. This would keep runtime discovery and operator-controlled installation from this PR, while restoring the RFC’s ability to support real frontend build outputs without rebuilding DeerFlow itself.

I do not think this needs to block proving the current page/action APIs, but the limitation and intended compatibility direction should be explicit before BrowserModule becomes a stable public API. Otherwise plugin authors may build against the single-file constraint and make later asset support unnecessarily difficult or breaking.

@AnnaSuSu

Copy link
Copy Markdown
Collaborator Author

@zhfeng I agree that the single-file module should not define the long-term packaging model. In 413a68f, the README, public BrowserModule docstring, PR description and plugin guide now explicitly identify the browser API/inline transport as experimental and describe the additive compatibility direction.

The follow-up is a distinct versioned manifest/asset-root declaration that can coexist with inline v1, while retaining namespaces, page/action interfaces and deployment-controlled installation. The guide calls out manifest allowlisting, traversal/symlink confinement, correct MIME types, revisioned immutable caching with private authentication preserved, and explicit transport negotiation. It also notes that relative chunks need an authenticated loading design for split-origin deployments; simply reusing the current Blob importer would not work, and a same-origin asset proxy is one option to evaluate.

These are documented compatibility requirements, not a claim that packaged static assets are implemented by this PR. This keeps the current slice focused on validating page/action APIs without presenting the single-file restriction as the final stable contract.

@Totoro-qaq

Copy link
Copy Markdown
Contributor

Thanks for the follow-up. I rechecked 413a68ff: the existing tool-collision and conversation-action isolation findings are addressed, and my earlier regression cases now pass. Local offline validation passed with 1,928 frontend tests (including four local regressions) and 274 relevant backend tests. Thanks also for making the experimental browser transport and the additive packaged-assets compatibility direction explicit.

One non-blocking edge case remains in resolveConversationActions: if a plugin incorrectly supplies available: async () => { throw new Error("unavailable"); }, the group is correctly omitted, but the returned Promise's rejection remains unhandled. I reproduced this with the production resolver; the current async-availability test covers a resolved Promise only. Consuming the rejection when rejecting a Promise return would complete that failure handling.

The callback contract requires a synchronous boolean, and the bundled bookmark plugin follows it, so I would not block this slice on that edge case. I found no additional blocking issue in this follow-up.

const actions = make("div", "", { class: "actions" });
// This is navigation metadata, never an authorization grant to a thread.
const link = make("a", words.open, {
href: `/workspace/chats/${encodeURIComponent(item.thread_id)}`,

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.

[P2] Preserve Custom Agent routing when reopening bookmarks

“Save last answer” is also available on Custom Agent chat pages, but this link always opens /workspace/chats/{id}. A bookmark created in /workspace/agents/{agent}/chats/{id} therefore reopens the default chat page instead of the originating agent-specific page; the default page uses useThreadStream with its default assistantId="lead_agent". Preserve or resolve the originating agent and generate the corresponding agent-specific conversation URL (following the existing pathOfThread routing rules). The toolbar context and stored bookmark currently retain only the thread ID for navigation, so those paths also need to carry or resolve the routing metadata. Please add a regression covering saving and reopening a Custom Agent conversation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in f78c206. The bookmark page now calls a host-provided openConversation(threadId) helper. It retrieves the thread through the authenticated API and uses the existing pathOfThread routing rules to preserve its originating Agent. This also fixes existing bookmarks without a data migration. Failed or aborted metadata lookups do not navigate, and surface cleanup cancels pending navigation.

The production Chromium regression now saves and reopens bookmarks from a default conversation, a custom-Agent toolbar and a custom-Agent sidebar, then sends another message and checks the actual run request uses the original Agent. All three pass. The full frontend unit suite passes (1,934 tests), as do lint, TypeScript, formatting, the production build and all 21 targeted E2E tests.

@AnnaSuSu

Copy link
Copy Markdown
Collaborator Author

Following up on the rejected async available callback reported above: fixed in f78c206. The resolver still requires synchronous factories and boolean availability, but now observes rejected Promise/thenable results before omitting the invalid group. The same handling covers an accidentally async conversationActions factory.

Added unit regressions for rejecting factories, availability callbacks and thenables, plus a production browser regression checking that rejecting async availability produces no page error and leaves healthy actions usable. Validation: 1,934 frontend unit tests and 21 targeted production Chromium E2E tests passed; lint, TypeScript, formatting and production build also passed.

@KevinAndrewDong KevinAndrewDong 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.

task_tool now carries the parent run immutable extension snapshot into both tool assembly and SubagentExecutor, but the durable batch_task path does not preserve the same generation boundary.

SubagentBatchService._execute_item() calls get_available_tools() and constructs SubagentExecutor without extensions=. Outside the run-bound assembly context, tool assembly therefore falls through get_agent_build_extensions() to the process-global snapshot, while the executor later falls back directly to get_loaded_extensions(). An unrelated set_loaded_extensions() or second app instance in the same process can consequently make a Gateway run and its durable-batch delegation use different extension generations.

I do not think LoadedExtensions should be persisted in execution_spec: durable items survive restarts, so a Python snapshot cannot be part of the durable contract. Could the app-scoped SubagentBatchService instead capture app.state.extensions at startup, like its other runtime dependencies, and pass that snapshot to both get_available_tools(..., extensions=...) and SubagentExecutor(..., extensions=...)?

That would keep all items executed by one Gateway instance on its immutable extension generation, while naturally adopting the new generation after a restart.

@github-actions github-actions Bot added the area:agents Agents, subagents, graph wiring, prompts, langgraph.json label Sep 21, 2026
@AnnaSuSu

Copy link
Copy Markdown
Collaborator Author

@KevinAndrewDong addressed your review in fc20b5b.

The generation mismatch is reproducible: replacing the process snapshot before durable tool assembly selects the replacement plugins; replacing it after assembly can split the tool and executor generations.

The fix pins one LoadedExtensions object in SubagentBatchService at construction. Gateway supplies app.state.extensions, and both get_available_tools and SubagentExecutor receive that same object, including an explicitly empty snapshot. Standalone services capture the process default once. execution_spec remains unchanged; recovered items use their new worker's generation after restart.

The regression exercises production tool assembly and the real executor's snapshot selection with a scripted graph. It covers replacements before assembly and before execution, explicit and implicit snapshots, empty snapshots, and recovery under a new worker. A separate Gateway lifespan test checks app-scoped injection. Persistence and model responses in these focused tests are fixtures; no live provider verification is claimed.

Validation: 140 focused backend tests and 149 blocking-I/O tests passed, along with format/lint and the Gateway import checks in the full suite. Full backend: 18,029 passed / 15 failed / 206 skipped; all 15 failures reproduce on clean baseline 29d28573, with no new failures. The production frontend and prior browser validation are unchanged by this follow-up.

@WillemJiang WillemJiang added this to the 2.1.0 milestone Sep 22, 2026

@zhfeng zhfeng left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please don't close the RFC issue since this is only a partial implementation and I think we need some follow-up PRs to enhance the frontend pages. Thanks a lot for your contributions!

@WillemJiang
WillemJiang merged commit e2f19d8 into bytedance:main Sep 22, 2026
24 checks passed
@WillemJiang WillemJiang changed the title feat(plugins): 全栈插件接口与书签示例 / full-stack plugin APIs and bookmarks feat(plugins): full-stack plugin APIs and bookmarks Sep 22, 2026
@WillemJiang WillemJiang modified the milestones: 2.1.0, 2.2.0 Sep 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agents Agents, subagents, graph wiring, prompts, langgraph.json area:backend Gateway / runtime / core backend under backend/ area:ci GitHub Actions, CI config, repo tooling area:deps Dependency manifests / lockfiles area:docs Documentation and Markdown only area:frontend Next.js frontend under frontend/ needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/XL PR changes 700+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants