Files
Arvin Xu 1e8767eb2a ♻️ refactor(mecha): extract agent config resolution into a shared package (#19476)
There are two Mecha layers doing the same job against different data sources.
The browser assembles an agent's effective run config in
`src/services/chat/mecha` (~2300 lines including its context builder); the
server assembles the same thing in `apps/server/src/modules/Mecha` plus an
851-line `serverCallLlmContextBuilder` that inlines the resolution alongside
reads of six database models. Both wrap the same shared `MessagesEngine`, and
the primitives underneath (`getActivePluginIds`, `ToolResolver`,
`resolveTopicReferences`) were extracted long ago — only the orchestration
above them was left duplicated.

Spot-checked rather than inferred from size: both sides independently compute
`enabledToolIds`, `enabledManifests` and the merged `systemRole` from an agent
config, producing the same shape from different sources.

This moves the browser's resolution rules into `@lobechat/mecha` unchanged,
behind an `AgentConfigSnapshot` of everything they previously read from
Zustand. The browser now gathers that snapshot — 76 lines — and delegates.
Nothing else changes: the barrel re-exports the same names, so no call site
moved, and `getTargetAgentId` stays behind because it reads the active agent.

The package takes the name both hosts already use for this layer, because the
rest of it follows: context engineering, tool composition and model-parameter
resolution are duplicated the same way and belong in the same home. A resolver
is the first inhabitant, not the whole tenancy.

It is a package of its own rather than part of `@lobechat/agent-runtime`
because the runtime keeps exactly three runtime dependencies — everything
heavy is type-only — and that is what let the CLI adopt it. These rules need
`builtin-agents`, which alone pulls in eight builtin-tool packages. Folding
them in would invert the property that makes the runtime portable. The runtime
also never resolves config; it reads `state.metadata.agentConfig` as an opaque
cast for a single field, so resolution belongs to the host that starts it.

`canManage` is an input rather than something the rules compute. The browser
answers it from the cache its picker populates and the server from
`isResourceAuthorOrAdmin`, which is asynchronous — a seam that only works if
the decision arrives already made.

Behaviour is pinned by the existing suite: all 77 of
`agentConfigResolver.test.ts` pass untouched, and they mock the stores, so they
exercise the new gathering and the extracted rules end to end. The package also
has its own suite that builds the snapshot by hand and never mocks a store, so
a store read creeping back in fails there even while the client tests still
pass.

The server and a device adopt this in later PRs — one host at a time, so a
regression stays attributable. For the device the payoff is the point: its
context port stops being an 851-line problem and becomes a snapshot to fill.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:16:27 +08:00

20 lines
618 B
JSON

{
"name": "@lobechat/mecha",
"version": "0.0.1",
"description": "Host-agnostic Mecha layer: resolving an agent's effective run configuration and the context it runs on",
"main": "src/index.ts",
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage --silent='passed-only'"
},
"dependencies": {
"@lobechat/builtin-agents": "workspace:*",
"@lobechat/builtin-tool-page-agent": "workspace:*",
"@lobechat/builtin-tool-task": "workspace:*",
"@lobechat/context-engine": "workspace:*",
"@lobechat/types": "workspace:*",
"debug": "^4.4.3",
"immer": "^10.1.3"
}
}