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/CHANGELOG.md +185 -0
- package/LICENSE +201 -0
- package/README.md +233 -0
- package/README_EN.md +237 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.js +2251 -0
- package/lib/endpoints.d.ts +16 -0
- package/lib/endpoints.js +29 -0
- package/lib/index.d.ts +261 -0
- package/lib/index.js +1481 -0
- package/lib/remote.d.ts +25 -0
- package/lib/remote.js +49 -0
- package/lib/typert.d.ts +48 -0
- package/lib/typert.js +217 -0
- package/package.json +104 -0
- package/scripts/install.ps1 +30 -0
- package/scripts/install.sh +29 -0
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
|
+
[](https://www.npmjs.com/package/dsh-agent-persona)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](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
|
+

|
|
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
|
+

|
|
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).
|
package/cordis.patch.yml
ADDED
|
@@ -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
|