Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/assistant-ui/config-mcp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assistant-ui/config-skills.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assistant-ui/surface.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assistant-ui/thread-tool-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion packages/app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
"@we/template-default": "workspace:*",
"@we/template-shell": "workspace:*",
"@we/editor": "workspace:*",
"@we/globe-widget": "workspace:*"
"@we/globe-widget": "workspace:*",
"@we/module-assistant": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.10.2",
Expand Down
13 changes: 13 additions & 0 deletions packages/app-shell/src/frameworks/solid/stores/DatasetStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,17 @@ export function DatasetStoreProvider(props: ParentProps) {
// The *global* uri, never the local uuid — a uuid is local per-agent, so a call id derived
// from one would differ on every peer and each would join a call only they can see.
datasetUri: () => currentDataset()?.sharedUrl ?? null,
// The personal root dataset, for modules with agent-scoped models (assistant config etc.).
rootDataset: () => rootDataset() ?? null,
selfId: () => session.me()?.did ?? null,
ephemeral: session.ephemeralPort,
// Backend connection details for modules that declare this backend and talk to its HTTP
// surface directly. Degrades to null like presence on hosts without one.
connection: () => {
const port = session.port();
const token = session.token();
return port !== undefined && token !== undefined ? { port, token, url: session.serverUrl() } : null;
},
});

// Converts null → undefined so that when JSON-serialised into an ORM WHERE clause,
Expand Down Expand Up @@ -257,6 +266,9 @@ export function DatasetStoreProvider(props: ParentProps) {
if (rootP) {
// Ensure all models are registered (handles new models added after initial creation)
await session.backendPorts()!.schemas.installRoot(rootP);
// Agent-scoped module models install to the personal root dataset at boot (space-scoped
// ones install per space switch) — see ModuleDefinition.agentModels.
await session.backendPorts()!.schemas.installModules(rootP, moduleRegistry.agentModels());
setRootDataset(rootP);

const settings = await AgentSettings.findOne(rootP);
Expand Down Expand Up @@ -296,6 +308,7 @@ export function DatasetStoreProvider(props: ParentProps) {
console.log('DatasetStore: creating root dataset');
const perspective = (await lifecycle.create('we-root')).handle as DatasetProxy;
await session.backendPorts()!.schemas.installRoot(perspective);
await session.backendPorts()!.schemas.installModules(perspective, moduleRegistry.agentModels());

const settings = await AgentSettings.create(perspective, {
currentTemplateId: 'default',
Expand Down
3 changes: 3 additions & 0 deletions packages/app-shell/src/shared/registries/bundledModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*
* Adding a module here plus an id in `we-seed.json` is the whole install story for now.
*/
import { assistantModule } from '@we/module-assistant';
import { callModule } from '@we/module-call';
import { createGlobeModule } from '@we/module-globe';
import { notesModule } from '@we/module-notes';
Expand All @@ -33,6 +34,8 @@ export interface ActivationDeps extends BundledModuleDeps {

export const bundledModules: Record<string, BundledModuleFactory> = {
globe: ({ components }) => createGlobeModule(components.CesiumGlobe),
// Ships its own Solid components (declared via `frameworks`), so it takes nothing from the host.
assistant: () => assistantModule,
// Takes nothing from the host: every piece of its UI is a schema fragment, so it imports no
// framework at all.
notes: () => notesModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import type { ModuleStoreDeps } from '@we/module-shared';
export interface ModuleHostServices {
dataset?: () => DatasetHandle | null;
datasetUri?: () => string | null;
rootDataset?: () => DatasetHandle | null;
connection?: () => { url?: string; port?: number; token?: string } | null;
selfId?: () => string | null;
ephemeral?: EphemeralPort;
presence?: {
Expand Down Expand Up @@ -67,6 +69,8 @@ export function createModuleStoreDeps(framework: {

dataset: () => services.dataset?.() ?? null,
datasetUri: () => services.datasetUri?.() ?? null,
rootDataset: () => services.rootDataset?.() ?? null,
connection: () => services.connection?.() ?? null,
selfId: () => services.selfId?.() ?? null,

// A stable function that forwards, so a module capturing `deps.ephemeral` at construction still
Expand Down
15 changes: 12 additions & 3 deletions packages/app-shell/src/shared/registries/moduleRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const moduleRegistry = {
// Predicates are how existing data is found, so minting one outside the module's own subtree is
// not a bug to fix later — by the time it is noticed, data has been written under a name nobody
// can adjudicate. Refused at registration for the same reason an incompatible backend is.
const badPredicates = (definition.models ?? []).flatMap((model) =>
const badPredicates = [...(definition.models ?? []), ...(definition.agentModels ?? [])].flatMap((model) =>
modulePredicateViolations(definition.id, getModelPredicates(model as Parameters<typeof getModelPredicates>[0])),
);
if (badPredicates.length) {
Expand Down Expand Up @@ -136,7 +136,10 @@ export const moduleRegistry = {
// different moment: SDNA install (in `installSpaceSdna`) puts the *shape* in the perspective,
// while this puts the *class* where `model.create` / `$query` can resolve it by name. Without
// this one the panel renders and only writing a note fails.
for (const model of (definition.models ?? []) as ModelClass[]) {
// Space-scoped and agent-scoped models both register as resolvable classes; the same entity may
// appear in both lists (installed into both kinds of dataset), so dedupe by class.
const allModels = [...new Set([...(definition.models ?? []), ...(definition.agentModels ?? [])])];
for (const model of allModels as ModelClass[]) {
registerModel((model as unknown as { className: string }).className, model);
}

Expand All @@ -156,7 +159,8 @@ export const moduleRegistry = {
const entry = modules.get(id);
if (!entry) return;
for (const index of (entry.definition.slots ?? []).keys()) slotRegistry.remove(`${id}:${index}`);
for (const model of (entry.definition.models ?? []) as ModelClass[]) {
const allModels = [...new Set([...(entry.definition.models ?? []), ...(entry.definition.agentModels ?? [])])];
for (const model of allModels as ModelClass[]) {
unregisterModel((model as unknown as { className: string }).className);
}
delete moduleStores[id];
Expand Down Expand Up @@ -197,6 +201,11 @@ export const moduleRegistry = {
return moduleRegistry.all().flatMap((m) => m.definition.models ?? []);
},

/** Entity types modules install into the agent's root dataset — see `ModuleDefinition.agentModels`. */
agentModels(): unknown[] {
return moduleRegistry.all().flatMap((m) => m.definition.agentModels ?? []);
},

/**
* Every registered module that contributes an embedded application, in registration order.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/app-shell/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const alias = { '@shared': r('./src/shared'), '@solid': r('./src/frameworks/soli

// Tests that mount real Solid components/providers — they need jsdom, browser resolve
// conditions, and the solid transform. Everything else stays in the plain node project.
const SOLID_TESTS = ['tests/executorFreeBoot.test.tsx'];
// bundledModules imports the assistant module's Solid components through its definition.
const SOLID_TESTS = ['tests/executorFreeBoot.test.tsx', 'tests/bundledModules.test.ts'];

export default defineConfig({
test: {
Expand Down
34 changes: 34 additions & 0 deletions packages/module-system/assistant/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@we/module-assistant",
"version": "0.1.0",
"description": "AI-assistant feature module — threads/messages rendered from the dataset, replies written by the backend",
"type": "module",
"exports": {
".": { "import": "./src/index.ts", "types": "./src/index.ts" }
},
"scripts": { "test": "vitest run" },
"peerDependencies": {
"@coasys/ad4m": "*",
"@we/components": "workspace:*",
"@we/design-utils": "workspace:*",
"@we/models": "workspace:*",
"@we/module-shared": "workspace:*",
"@we/schema-shared": "workspace:*",
"solid-js": "^1.9.5"
},
"devDependencies": {
"@coasys/ad4m": "0.11.0",
"@solidjs/testing-library": "^0.8.10",
"@types/node": "^24.10.0",
"@we/components": "workspace:*",
"@we/design-utils": "workspace:*",
"@we/models": "workspace:*",
"@we/module-shared": "workspace:*",
"@we/primitives": "workspace:*",
"@we/schema-shared": "workspace:*",
"jsdom": "^27.2.0",
"solid-js": "^1.9.5",
"vite-plugin-solid": "^2.11.10",
"vitest": "^4.0.15"
}
}
Loading
Loading