Skip to content

dsh-assistants

Verified

@dr-co/dsh-assistants · v0.1.7 · MIT · Web UI

DSH plugin: assistants, virtual users and pair memory bound to sessions (host part)

Install

dsh plugin add @dr-co/dsh-assistants

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Readme

@dr-co/dsh-assistants

A DSH bundle/plugin that gives every session a who answers and a who is asking, and keeps the memory of that pair. It is useful when one DSH installation serves several assistants, people, bots, or scheduled jobs and each session needs a predictable persona without changing the underlying agent preset.

Why this is useful

One DSH installation can be shared by several people while giving each person an independent assistant and pair-specific context. For setups that would otherwise need several separate installations of products such as OpenClaw, one DSH host can provide the same kind of separation through session routing and virtual users, without claiming to replace every feature of those products.

The administration UI is the place to create and manage assistant and virtual-user variants. This is also useful for subagent workflows: you can define roles such as architect, programmer, and reviewer, then ask for a sequence such as “let the architect design a plan, then let the programmer implement it”. Each subagent is resolved with the prompt configured for the selected role.

Every assistant has its own pair-scoped memory: what it knows about a user and how that user asks it to respond. The memory follows the assistant–user pair rather than the transport or bot.

This package contains the host plugin and the browser client bundle. It does not patch DSH core and it does not replace the host's preset, tool, delegation, or transport implementation.

Installation and bundle metadata

Install the package using the normal plugin/bundle mechanism of the DSH host. The package is self-describing: package.json declares the dsh.bundle.patch metadata, and cordis.patch.yml inserts the dsh-assistants row into the host's plugin list. The same package metadata declares the browser entry as dsh.client; the host loads lib/client.js when the Web client is available. No manual edit to index.html or DSH core is required.

The bundle is intentionally opt-in through the host's plugin list. Its state is kept separately from DSH core and can be disabled or removed by the host's ordinary plugin controls. If a host does not implement the DSH bundle/plugin metadata loader, this package cannot install itself there; use a host version that supports that loader rather than applying the patch by hand.

Compatibility and limitations

The plugin uses public host services and optional integrations only. It does not promise an official DSH core patch API. In particular:

  • identity lookup for IM sessions requires the host's dshIm.describeConversation; without it, only the transport can be identified;
  • cron identity requires a schedule entry with a matching sessionId;
  • the subagentPersona.resolve composition hook is used only when the host exposes ctx.provide; older hosts keep inherited assistant behaviour;
  • unavailable browser slots are reported and skipped, so the settings card and composer controls may be absent on older Web hosts;
  • the client bundle is Web-only; the host part can still provide prompt, memory, and HTTP functionality without the browser client.

The fallback behaviour is deliberate: missing optional services do not make the plugin claim that DSH core supports a new API, and they do not prevent the rest of the plugin from loading.

What it models

Two independent axes:

  • assistant — a persona with one block of text about itself;
  • virtual user — a persona with a prompt, facts about the person, and the access mode that session runs under.

Both are resolved independently: their own rules, their own default, their own log line. A rule never assigns both axes.

The prompt the model sees is one section with four parts, each dropped when empty:

# who you are                             the assistant's own text
# who you are talking to                  user prompt + facts
# about the person you are talking to      memory au:<assistant>:<user>
# additional information about you         memory ua:<user>:<assistant>

Memory is keyed by the pair, never by the bot: the bot is transport. There are two buckets and only two:

key meaning written by
au:<assistant>:<user> what this assistant knows about this person the model, via pair_memory_save
ua:<user>:<assistant> how this person wants this assistant to answer the model, via pair_memory_save

Each bucket also has a summary — rau:<assistant>:<user> for au:, rua:<user>:<assistant> for ua: — and the prompt prints the summary when it exists, falling back to the raw rows. A summary records the row count it was built from, so "stale" is a comparison. POST /__as/summary recomputes one through the host's llm service; without that service it answers 503 llm-unavailable and the summary can be written by hand in the same field.

Everything about the assistant itself or about the person in general is a persona field — a human edits it, the model does not.

Matching

Rules are two ordered lists (assistants, users). A rule is a set of conditions plus assign; every condition present must hold, and a condition the session cannot answer makes the rule miss. First match wins. A rule with no conditions is the axis default.

users:
  - sender_id: "161161619"        # this person, in any chat
    assign: u-unera
  - bot_id: telegram_fc1c…        # everybody else who writes to this bot
    assign: u-marish
  - assign: u-unera               # the axis default

Conditions: transport (web, im:telegram, im:discord, cron), bot_id, group_id, chat_id, sender_id, regexp (over the conversation key).

Identity comes from the session's creator:

transport where the identity comes from
im dshIm.describeConversation(sessionId)
cron the task in the schedule store whose sessionId matches
web nothing but "web", until the person chooses in the form

Access mode

The mode belongs to the virtual user, not to the assistant: rights follow the person who arrived, not the character who answers. One person arrives, one mode applies, so there is nothing to reconcile.

Configuration

- id: dsh-assistants
  name: '@dr-co/dsh-assistants'
  config:
    promptLimit: 6000

promptLimit caps the rendered section; over the cap the tail is dropped and the loss is logged once. State lives in $DSH_HOME/storages/dsh-assistants.json, is written atomically, watched, and applied without a restart. The package ships defaults.json; it seeds that file once, on the first boot.

HTTP

GET  /__as/state                 personas, rules, defaults, overrides, memory
POST /__as/save                  replace personas / rules / defaults
POST /__as/session               manual override for one session
POST /__as/memory                add a row, or delete one by id
GET  /__as/diag[?session=…]      what this plugin thinks, and why

Not here yet

Subagent personas: on hosts exposing ctx.provide, the plugin registers subagentPersona.resolve(assistantId, { parent, child }). dsh-subagent calls it for a subagent_fork that passes assistant; the child gets that assistant, the user of the parent session and their pair memory. The plugin's own prompt section never renders for a child agent, so a fork without assistant runs without a persona.

The client bundle (composer pickers, names and icons on messages, the admin card) and the migration from dsh-agent-profiles are the next iteration.

Client half

The browser side is a real client plugin (the same shape @dr-co/dsh-skills uses), not a script injected into index.html: package.json declares dsh.client, the host loads lib/client.js and calls apply(ctx), and every contribution goes through the public slot registry.

slot what it draws
settings.section the admin page: assistants, users, both rule lists, sessions
conversation.input.left the two pickers in the composer row, scoped per session
conversation.chat.node the assistant's name and icon above a turn (shadows the official turn-process node and renders it inside, unchanged)

Strings live in src/client/locales.js (ru / en / zh); a test compares the key sets so a translation cannot silently miss a key.

Build and tests

npm install          # esbuild only
npm run build        # writes lib/client.js
npm test             # full Node test suite, including the built bundle
npm pack --dry-run    # inspect the public package file list

The CI job runs the build before the tests and checks the package with npm pack --dry-run. The package does not publish or install anything by itself.

The build is a single esbuild pass plus the window.__ModuleLoader__ wrapper the shell expects, so lib/client.js is reproducible from src/client/.

Client beacon

At the end of apply the bundle posts {ok, mounted, failed} to POST /__as/client-report; the host logs it as one line:

[dsh-assistants] client mounted=settings.section,sidebar.panellist,main failed=conversation.input.dock: not mounted on this host

Browser warnings never reach the server log, and "the page is not there" is exactly the report that needs a line in the journal rather than a DevTools session.