dsh-agent-persona 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README_EN.md ADDED
@@ -0,0 +1,237 @@
1
+ # dsh-agent-persona
2
+
3
+ Different system-prompt personas for different **workspaces** and different **sessions**.
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-agent-persona)](https://www.npmjs.com/package/dsh-agent-persona)
6
+ [![license](https://img.shields.io/badge/license-Apache--2.0-blue)](./LICENSE)
7
+ [![ci](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml/badge.svg)](https://github.com/awoodwhale/dsh-agent-persona/actions/workflows/ci.yml)
8
+
9
+ [中文](./README.md)
10
+
11
+ Note: the settings UI is in Chinese. The code, docs and this file are English.
12
+
13
+ ## What it does
14
+
15
+ A DSH deployment usually runs several jobs at once — frontend work, docs, alert triage, code review — and each
16
+ wants its own identity, tone and constraints. This plugin hands each of them its own persona, injected as a
17
+ **section of the system prompt**:
18
+
19
+ - **Many personas, each with its own reach.** One persona can apply to several workspace directories and several
20
+ sessions; a match is `exact`, `prefix`, `contains` or `regex`.
21
+ - **More specific wins.** A session-id rule beats a workspace rule, and `exact` beats `prefix`/`contains`/`regex`;
22
+ only equal specificity falls back to list order.
23
+ - **An explicit default persona.** Mark one persona as the default and it takes every session nothing else claimed
24
+ — at most one at a time. With none marked, those sessions keep DSH's own system prompt.
25
+ - **A place belongs to one persona.** Pointing a workspace or session at a new persona takes it away from whoever
26
+ held it, and the save reports whose claim moved.
27
+ - **Append or replace.** Keep DSH's identity line and put the persona after it, or drop that line and use only the
28
+ persona.
29
+ - **Empty text means "no persona here"** — add a rule for a place and leave the body empty to switch it off there.
30
+ - **An edit applies to the next message.** No restart: the body is re-read per assembly by file stamp.
31
+
32
+ Three surfaces, three jobs:
33
+
34
+ | Surface | Where | What for |
35
+ |---|---|---|
36
+ | **Settings page** | Settings → Agent 人设 | Create, edit, delete, duplicate and reorder personas; write the body, preview Markdown, let AI rework it |
37
+ | **Session view** | the **人设** tab on a conversation | Which persona this session matched and why, the **system prompt it actually sent** (rendered and source), and the same management UI inline |
38
+ | **Sidebar card** | the right bar of `dsh-better-sidebar` | Puts that same view into the sidebar (needs that plugin installed) |
39
+
40
+ Three preference switches (`编辑后自动保存` / `在对话页显示「人设」标签` / `注册到 dsh-better-sidebar`) appear both on the
41
+ manage page and in **Settings → General** — the latter is not affected by them, so a display position can always
42
+ be switched back on.
43
+
44
+ ## Why it outranks AGENTS.md
45
+
46
+ DSH turns workspace instructions (`AGENTS.md` and friends) into a **user-role** message, and that message's own
47
+ intro says *"They do not override system, developer, or direct user instructions."* This plugin writes a
48
+ **system-prompt section** instead (section name `agent-persona`, order 1, right after the identity line).
49
+
50
+ That ordering is the framework's, not something the plugin claims — which is also why editing files inside the
51
+ workspace cannot touch this layer.
52
+
53
+ ## Install
54
+
55
+ **Tested against DSH `0.1.5-rc.1` (the author's development environment) and `0.1.5-rc.2`.** That is what
56
+ `package.json`'s `dsh.compatibility` declares — it means "we ran on these", not a promise of compatibility work
57
+ for older or newer releases; please verify on yours.
58
+
59
+ The sidebar card additionally needs **`dsh-better-sidebar`**: without it the `注册到 dsh-better-sidebar` switch is not
60
+ shown at all (there is nothing to register into), and everything else works as usual.
61
+
62
+ ```bash
63
+ dsh plugin --profile web add dsh-agent-persona
64
+ ```
65
+
66
+ Then **restart `dsh web` once**: profile bundles are read at startup, and that is where the plugin row lives.
67
+
68
+ After the restart:
69
+
70
+ - the **Agent 人设** entry in settings;
71
+ - `cat ~/.dsh/dsh-agent-persona/state.json` — the load heartbeat: which module was loaded, how many personas,
72
+ where the store is.
73
+
74
+ Local install scripts live in `scripts/install.sh` and `scripts/install.ps1`.
75
+ To remove: `dsh plugin --profile web remove dsh-agent-persona`, restart; the persona file is left alone.
76
+
77
+ ## Using it
78
+
79
+ ![persona list](./docs/images/settings-list.png)
80
+
81
+ **New persona** → add rows under reach, pick a workspace or a session → write the body → switch it on → save.
82
+
83
+ - A **new persona starts disabled**, so adding one changes nothing until you say so.
84
+ - Reach is a list of rows. Each row picks a kind (**workspace directory** or **a session**) and then the target
85
+ from a dropdown; switching a row's kind clears its value.
86
+ - The dropdowns come from DSH's own workspace and session lists — sessions carry their **title**, working
87
+ directory and "minutes ago", so you do not need to know a session id.
88
+ - Boundaries like prefixes and regexes are available through **type it yourself…**.
89
+ - A persona with **no rows applies nowhere** (it is a draft) unless it is marked as the default.
90
+
91
+ ### Reading what a session actually said
92
+
93
+ The button next to the session dropdown opens a **read-only** transcript: your inputs on the right, the agent's
94
+ replies on the left.
95
+
96
+ - **Only what you actually typed.** Workspace instructions (`AGENTS.md`), runtime context snapshots, skill
97
+ directory notes — anything the harness or another plugin injected as a user-role message — are left out, with a
98
+ single line at the top saying how many were hidden.
99
+ - Long messages are clipped; **展开全文** re-reads just that one and replaces that bubble, and **收起全文** puts the
100
+ short text back.
101
+ - **跳到最新** shows only the last user input and everything after it.
102
+
103
+ ### The 人设 tab on a conversation
104
+
105
+ ![session view](./docs/images/view-persona.png)
106
+
107
+ **人设 / 提示词 / 管理**:
108
+
109
+ - **人设** — which persona matched, **which of its rules matched**, the body as Markdown, its own state markers
110
+ (in use / disabled, whether it is the default, its injection mode), and an inline editor (name, default switch,
111
+ injection mode, body). Editing shows 未保存 · 点击保存 in the card header.
112
+ - **提示词** — the prompt this session **actually sent**, with rendered/source views, copy and refresh. With no
113
+ persona matched it shows DSH's own prompt verbatim.
114
+ - **管理** — the same cards as the settings page, in place.
115
+
116
+ ### The three preferences
117
+
118
+ | Switch | Default | Meaning | Takes effect |
119
+ |---|---|---|---|
120
+ | 编辑后自动保存 | off | Save the editor about a second after typing stops | Immediately; rule rows (workspace/session) deliberately do **not** autosave, so a half-made choice is never stored |
121
+ | 在对话页显示「人设」标签 | on | Register the conversation view or not | Immediately (registered/unregistered at runtime) |
122
+ | 注册到 dsh-better-sidebar | on | Register the card with the sidebar plugin | Immediately; **not shown at all when that plugin is absent** |
123
+
124
+ Preferences live in the persona file's `prefs` field — not in the browser — so they share the data's scope. The
125
+ browser keeps only a mirror, used to decide at load whether to register the views.
126
+
127
+ ## Injection mode: append or replace
128
+
129
+ | Mode | Effect |
130
+ |---|---|
131
+ | **Append** (default) | DSH's own identity sentence stays; the persona follows it |
132
+ | **Replace** | DSH's identity sentence and the deployment's own persona line are dropped; only this persona remains |
133
+
134
+ Replace only touches what the deployment itself wrote. If an Agent preset shadows that section in its own scope,
135
+ the plugin leaves it alone — someone else's identity wins.
136
+
137
+ ## How matching works
138
+
139
+ On every assembly:
140
+
141
+ 1. disabled personas are skipped;
142
+ 2. any matching row makes a persona a candidate, compared by specificity (session id 3 vs workspace 1; exact beats
143
+ prefix/contains/regex);
144
+ 3. equal specificity falls back to list order (`⋯` → move up/down);
145
+ 4. a persona with **no rows does not take part** (draft), unless it is the marked default;
146
+ 5. nothing matched → use the default persona if one is marked, otherwise inject nothing;
147
+ 6. a winner whose body is empty injects nothing — that is how you say "no persona here".
148
+
149
+ Only `exact` normalises paths (`/a/project/` equals `/a/project`); `prefix`, `contains` and `regex` compare the
150
+ string you typed. Empty values, a broken regex, or a missing field on the session simply do not match — never an
151
+ error.
152
+
153
+ ## Files and configuration
154
+
155
+ ```
156
+ ~/.dsh/dsh-agent-persona/
157
+ ├── personas.json # personas + preferences, mode 600, written atomically
158
+ └── state.json # load heartbeat, rewritten on every load
159
+ ```
160
+
161
+ The body may use `{{model}}` and `{{cwd}}`; other `{{...}}` are de-braced (an unregistered variable would make the
162
+ whole assembly throw, so it is guarded here).
163
+
164
+ Configuration (a Schemastery schema, validated at load — a wrong type **fails the load** instead of being ignored):
165
+
166
+ | Config | Default | Meaning |
167
+ |---|---|---|
168
+ | `storePath` | `''` | Where personas live; empty means `$DSH_HOME/dsh-agent-persona/personas.json` |
169
+ | `tuneProvider` | `''` | Provider for the AI-tuning button; empty means DSH's current model |
170
+ | `tuneModel` | `''` | Model id for the same |
171
+
172
+ ### One store per profile (optional)
173
+
174
+ The store is **global by default** (`$DSH_HOME/dsh-agent-persona/personas.json`, the same convention other plugins
175
+ use for their state), so profiles on one machine share one set of personas. To separate a profile, override the
176
+ path in **that profile's** `cordis.patch.yml` (override the field — do **not** insert another row):
177
+
178
+ ```yaml
179
+ # ~/.dsh/profiles/<profile>/cordis.patch.yml
180
+ - id: agent-persona
181
+ config:
182
+ storePath: /Users/you/.dsh/profiles/<profile>/personas.json
183
+ ```
184
+
185
+ `DSH_HOME` isolates everything at once (sessions, other plugins' state). The path in force is always on the
186
+ Agent 人设 page (`保存在 …`), so it is never a mystery which file you are editing.
187
+
188
+ ## Honest notes
189
+
190
+ - **A persona is a behavioural constraint, not a security boundary.** It shapes what the model says; it does not
191
+ stop tools.
192
+ - `personas.json` is an **ordinary file**: any session with file tools can write it. This plugin does **not**
193
+ register a `tools.guard`. If you need "a conversation inside the workspace cannot change the persona", do it at
194
+ the machine level (`chmod 400` plus a different owner, or a restricted sandbox).
195
+ - Only the **first** match is used — no concatenation. Matching is a linear scan; dozens of rules are fine,
196
+ hundreds want a redesign.
197
+ - Install once per profile: two rows with the same id in one process make the composition fail.
198
+ - **Test boundary**: the suite is unit tests plus source audits (134 host assertions, 15 client assertions, and a
199
+ generated-artifact drift check). It does not drive a browser — UI behaviour is checked by hand today.
200
+
201
+ ## Development
202
+
203
+ Sources are TypeScript; `lib/` is build output (not in git, built by `prepare` before `npm publish`):
204
+
205
+ ```
206
+ src/index.ts host half: store, matching, the system-prompt section, the remote service
207
+ src/client.ts client half: settings page + session view + sidebar card (bundled to lib/client.js)
208
+ src/endpoints.ts single source for the remote surface (generates src/remote.ts and src/typert.ts)
209
+ scripts/ the generator and install scripts
210
+ test/ unit tests and source audits
211
+ docs/ architecture, development notes, design decisions
212
+ ```
213
+
214
+ ```bash
215
+ npm install
216
+ npm run build # generate:remote → tsc → esbuild (writes lib/)
217
+ npm run typecheck # both tsconfigs, no errors
218
+ npm test # 134 + 15 assertions plus the generated-artifact drift check
219
+ dsh --profile web --dump-config | grep -c 'id: agent-persona' # composition self-check, expects 1
220
+ ```
221
+
222
+ How edits take effect:
223
+
224
+ - **`src/**` always needs `npm run build`** — `lib/` is output.
225
+ - Changing the **content** of the client artifact: the client recomposes, so **refresh the page**; do not rename
226
+ the file.
227
+ - Changing the **content** of the host artifact: ESM caches modules by URL, so it does **not** reload — restart
228
+ `dsh web`.
229
+ - Changes to `dsh.bundle.patch` or the profile's `bundles`: restart.
230
+ - Never hand-edit the generated `src/remote.ts` / `src/typert.ts`: edit `src/endpoints.ts` and run
231
+ `npm run generate:remote`; `npm test` fails if they drift.
232
+
233
+ More in [docs/development.md](./docs/development.md) and [docs/architecture.md](./docs/architecture.md).
234
+
235
+ ## License
236
+
237
+ Apache-2.0, see [LICENSE](./LICENSE).
@@ -0,0 +1,8 @@
1
+ # Bundle patch: mounts this package's host half as one Cordis loader row.
2
+ #
3
+ # The same row is the browser roster row — the package declares `dsh.client`, so
4
+ # client-modules discovers `./lib/client.js` from this package's package.json and
5
+ # serves it to the Web app.
6
+ - insert:
7
+ - id: agent-persona
8
+ name: dsh-agent-persona