dsh-agent-persona 0.2.0 → 0.2.1
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 +25 -0
- package/README.md +8 -3
- package/README_EN.md +5 -3
- package/lib/client.js +10 -2
- package/lib/index.d.ts +24 -0
- package/lib/index.js +84 -0
- package/package.json +1 -1
- package/scripts/install.ps1 +10 -5
- package/scripts/install.sh +9 -5
- package/src/client.ts +18 -2
- package/src/index.ts +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.1] - 2026-11-08
|
|
8
|
+
|
|
9
|
+
这一版修了两个实际会踩到的问题,另外把文档和安装脚本里过时的说法清理了一遍。
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **用「独占提示词」的 Agent 预设时,人设一个字都发不出去。** 这种预设会声明 `complete: true`,DSH 在
|
|
14
|
+
组装提示词的最后一步会把其他 section 全丢掉,本插件那一节也没了,而且不报错。现在插件在组装结束后
|
|
15
|
+
看一眼结果:发现只剩预设那一段、人设没进去,就把人设补进去——追加接在后面,替换整段换掉。
|
|
16
|
+
插件卸载时会把改动还原。
|
|
17
|
+
- 顺手把这层包装写稳了:宿主不让改方法时只跳过这个功能,不影响插件其他部分;插件卸载后即使别的插件
|
|
18
|
+
也包过一层,这里也不会继续往提示词里塞人设。
|
|
19
|
+
- **「提示词」不再慢一拍。** 以前打开着这个标签去发消息,标签里还是上一轮的 system prompt,得手动点
|
|
20
|
+
刷新或者整页刷新才更新。现在一轮跑完会自动重新读一次;正在编辑人设时不会动你的草稿。
|
|
21
|
+
|
|
22
|
+
### Tests
|
|
23
|
+
|
|
24
|
+
- 补了这些情况的测试:独占提示词下追加 / 替换 / 空正文、普通提示词不重复塞、包装还原、宿主报错时原样
|
|
25
|
+
返回,以及「插件装好后确实会包上 `assemble`」这一层。
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- 文档和安装脚本跟上现状:重启说明补了 systemd 托管的做法,插件详情页「界面与功能」和四个界面入口写
|
|
30
|
+
清楚了;开发文档里的断言数量、`dsh.bundle.patch` 字段名、远程方法数(16 个)一并改正。
|
|
31
|
+
|
|
7
32
|
## [0.2.0] - 2026-11-07
|
|
8
33
|
|
|
9
34
|
插件页自己会管配置了,图标、显示名和最新的 DSH 也对上了。
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
dsh plugin --profile web add dsh-agent-persona
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
装完**重启一次 `dsh web`**。profile 的 bundles
|
|
25
|
+
装完**重启一次 `dsh web`**。profile 的 bundles 只在启动时读取,插件行就是从这里挂进去的。如果你的宿主由 systemd 之类的服务托管(比如本机的 `dsh-web-wood`),改用 `sudo systemctl restart dsh-web-wood`,别自己 kill 掉进程再手起。重启后设置左栏会多出 **Agent 人设**;`~/.dsh/dsh-agent-persona/state.json` 里也有加载心跳,写明了加载的文件、人设存哪、一共几条。
|
|
26
26
|
|
|
27
27
|
升级、卸载同样是 `dsh plugin --profile web update|remove dsh-agent-persona`,重启一次后生效。人设文件不会跟着删,也不会被覆盖。仓库里另有本地安装脚本:`bash scripts/install.sh`(Windows 用 `scripts/install.ps1`)。
|
|
28
28
|
|
|
@@ -54,7 +54,7 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
54
54
|
|
|
55
55
|
关掉「在设置左栏显示「Agent 人设」」只是从设置导航里隐藏,插件详情页仍在,可以继续改人设、随时开回来。插件列表里中文名是「Agent 人设」、英文名是「Agent Persona」,说明文字来自 `locale/`,图标是包根的 `icon.svg`,不跟随主题色。
|
|
56
56
|
|
|
57
|
-

|
|
58
58
|
|
|
59
59
|
## 人设列表与编辑
|
|
60
60
|
|
|
@@ -83,7 +83,7 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
83
83
|
选中一条会话后,顶部有 **人设 / 提示词 / 管理** 三个面板:
|
|
84
84
|
|
|
85
85
|
- **人设**:命中了哪条人设、依据是哪几条规则、正文渲染结果,以及就地编辑器;
|
|
86
|
-
- **提示词**:这条会话**实际发出去的** system prompt
|
|
86
|
+
- **提示词**:这条会话**实际发出去的** system prompt,可切换渲染/源码、复制、刷新。一轮对话结束时它会自己重新读一次,不用手动点刷新;如果你正开着编辑框,它不会把你的改动冲掉;
|
|
87
87
|
- **管理**:和设置页同一套卡片,就地新建、启用、编辑、排序、复制、删除。
|
|
88
88
|
|
|
89
89
|
旁边的只读会话记录弹窗只显示你真正敲进去的内容,插件注入的「看起来像用户消息」的内容会折叠成一行提示;长消息可单条展开,也能跳到最新。
|
|
@@ -131,6 +131,11 @@ dsh plugin --profile web add dsh-agent-persona
|
|
|
131
131
|
|
|
132
132
|
正文里可以用 `{{model}}` 和 `{{cwd}}`,渲染时替换成当前值;其它 `{{...}}` 会被去掉,不会让整个组装报错。
|
|
133
133
|
|
|
134
|
+
还有一种情况得说清楚:有些 Agent 预设的人设段会声明 `complete: true`,意思是「这一节就是完整提示词」。
|
|
135
|
+
DSH 组装到最后会把别的 section 全扔掉,插件写的那节也一样,所以这种预设下人设看起来配好了、实际没发出去。
|
|
136
|
+
现在插件会在组装结束后检查一遍:要是只剩预设那一段、人设没进去,就把它补进去——追加接在后面,替换则
|
|
137
|
+
整段换成这条人设。换句话说,遇到这种预设时上面「别人的身份提示优先」就不作数了,那段提示词是唯一的位子。
|
|
138
|
+
|
|
134
139
|
## 数据与安全
|
|
135
140
|
|
|
136
141
|
```
|
package/README_EN.md
CHANGED
|
@@ -22,7 +22,7 @@ Requires DSH `0.2.0-rc.2` or newer; this release is tested on `0.2.2-rc.1` (full
|
|
|
22
22
|
dsh plugin --profile web add dsh-agent-persona
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Then **restart `dsh web` once**. Profile bundles are read at startup, and the plugin registers itself from there.
|
|
25
|
+
Then **restart `dsh web` once**. Profile bundles are read at startup, and the plugin registers itself from there. If a service manager runs the host (this machine's `web` profile is the systemd unit `dsh-web-wood`), restart that instead: `sudo systemctl restart dsh-web-wood`. Don't kill the process and start `dsh web` by hand.
|
|
26
26
|
|
|
27
27
|
After the restart the settings sidebar has an **Agent 人设** page, and `~/.dsh/dsh-agent-persona/state.json` contains a load heartbeat saying which file was loaded, where personas are stored, and how many there are.
|
|
28
28
|
|
|
@@ -56,7 +56,7 @@ The plugin detail page has a new **界面与功能** (Interface and features) bl
|
|
|
56
56
|
|
|
57
57
|
Turning off **在设置左栏显示「Agent 人设」** only hides the page from the settings navigation. The plugin detail page remains, so you can keep editing and turn the entry back on at any time. In the plugin list the display name is **Agent 人设** in Chinese and **Agent Persona** in English; the description comes from `locale/`, and the icon is `icon.svg` at the package root (it does not follow the theme color).
|
|
58
58
|
|
|
59
|
-

|
|
60
60
|
|
|
61
61
|
## Personas: list and editor
|
|
62
62
|
|
|
@@ -85,7 +85,7 @@ With **保存人设版本历史** on, every body save appends a diff to `persona
|
|
|
85
85
|
Select a session and the top of the view has **人设 / 提示词 / 管理** (persona / prompt / manage):
|
|
86
86
|
|
|
87
87
|
- **人设**: which persona matched, which rules matched, the rendered body, and an inline editor;
|
|
88
|
-
- **提示词**: the system prompt **actually sent** for this session, with rendered/source toggle, copy, and refresh;
|
|
88
|
+
- **提示词**: the system prompt **actually sent** for this session, with rendered/source toggle, copy, and refresh. It also reloads itself when a turn finishes, so you usually don't need the refresh button — and it won't clobber an editor you have open;
|
|
89
89
|
- **管理**: the same cards as the settings page, for creating, enabling, editing, reordering, duplicating, and deleting in place.
|
|
90
90
|
|
|
91
91
|
Next to it is a read-only conversation log: it shows only what you actually typed, and plugin-injected content that looks like a user message is collapsed into a one-line notice. Long messages can be expanded individually, and you can jump to the latest one.
|
|
@@ -133,6 +133,8 @@ Of the four match types, only `完全一致` (exact) normalizes paths (`/a/proje
|
|
|
133
133
|
|
|
134
134
|
The body supports `{{model}}` and `{{cwd}}`, substituted with current values at render time. Other `{{...}}` placeholders are removed rather than failing the whole assembly.
|
|
135
135
|
|
|
136
|
+
One more case worth spelling out: some Agent presets declare `complete: true` on their persona section, meaning "this section is the whole prompt". At the end of assembly DSH throws away every other section, including the one this plugin writes, so the persona looks configured but never reaches the model. The plugin now checks the result after assembly: if only the preset's section is left and the persona is missing, it puts the persona in — appended after it for Append, replacing the whole text for Replace. With such a preset the "someone else's identity wins" rule above no longer applies: that section is the only slot there is.
|
|
137
|
+
|
|
136
138
|
## Data and security
|
|
137
139
|
|
|
138
140
|
```
|
package/lib/client.js
CHANGED
|
@@ -883,7 +883,11 @@
|
|
|
883
883
|
* session it is showing; otherwise it keeps rendering the one it was first opened for.
|
|
884
884
|
*/
|
|
885
885
|
componentDidUpdate(previous) {
|
|
886
|
-
if (this.sessionId() !== sessionIdOf(previous))
|
|
886
|
+
if (this.sessionId() !== sessionIdOf(previous)) {
|
|
887
|
+
void this.load();
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (previous.running === true && this.props.running === false && this.state.editing !== true) void this.load();
|
|
887
891
|
}
|
|
888
892
|
sessionId() {
|
|
889
893
|
return sessionIdOf(this.props);
|
|
@@ -1144,6 +1148,10 @@
|
|
|
1144
1148
|
);
|
|
1145
1149
|
}
|
|
1146
1150
|
}
|
|
1151
|
+
const PersonaViewLive = (props) => {
|
|
1152
|
+
const running = props.useSession((snapshot) => snapshot?.running === true);
|
|
1153
|
+
return h(PersonaView, { ...props, running });
|
|
1154
|
+
};
|
|
1147
1155
|
const dotState = (persona) => {
|
|
1148
1156
|
if (persona === void 0) return "off";
|
|
1149
1157
|
if (!persona.enabled) return "off";
|
|
@@ -3011,7 +3019,7 @@ ${state.proposal.trim()}
|
|
|
3011
3019
|
id: "agent-persona",
|
|
3012
3020
|
order: 30,
|
|
3013
3021
|
label: () => "\u4EBA\u8BBE"
|
|
3014
|
-
},
|
|
3022
|
+
}, PersonaViewLive));
|
|
3015
3023
|
};
|
|
3016
3024
|
const dropTabSeat = () => {
|
|
3017
3025
|
if (!tabSeatMounted) return;
|
package/lib/index.d.ts
CHANGED
|
@@ -263,6 +263,30 @@ export declare const applyPersonaMode: (assembly: any, { mode, deploymentPrefix
|
|
|
263
263
|
deploymentPrefix: any;
|
|
264
264
|
mode: any;
|
|
265
265
|
}) => any;
|
|
266
|
+
/**
|
|
267
|
+
* Fold a persona into a prompt that one `complete: true` section owns.
|
|
268
|
+
*
|
|
269
|
+
* An agent preset may declare its own persona with `complete: true`; the registry then hands back that single
|
|
270
|
+
* section as the whole system prompt, and it does so *after* the `system-prompt/assemble` waterfall — so no
|
|
271
|
+
* listener can add a section of its own, and this plugin's section is discarded before it ever renders. The
|
|
272
|
+
* only place a persona can still live is inside that section.
|
|
273
|
+
* @param assembly - an assembled prompt.
|
|
274
|
+
* @param persona - `{ text, mode }` of the persona that claims this assembly, or `undefined`.
|
|
275
|
+
* @returns the assembly with the persona folded in, or the input when nothing applies.
|
|
276
|
+
*/
|
|
277
|
+
export declare const applyCompletePersona: (assembly: any, persona: any) => any;
|
|
278
|
+
/**
|
|
279
|
+
* Reach the prompts a `complete: true` section owns.
|
|
280
|
+
*
|
|
281
|
+
* `assemble()` is wrapped instead of listening to the waterfall because the registry collapses the section
|
|
282
|
+
* list *after* dispatching it: the waterfall's sections are overwritten by the complete one. Disposal restores
|
|
283
|
+
* the original method, so unloading the plugin leaves no trace.
|
|
284
|
+
* @param systemPrompt - the prompt registry service.
|
|
285
|
+
* @param readPersona - resolves `{ text, mode }` for one assembly context, or `undefined`.
|
|
286
|
+
* @param warn - diagnostic sink.
|
|
287
|
+
* @returns the disposer that restores `assemble`.
|
|
288
|
+
*/
|
|
289
|
+
export declare const bridgeCompletePrompt: (systemPrompt: any, readPersona: any, warn?: () => void) => () => void;
|
|
266
290
|
/**
|
|
267
291
|
* Session ids the user has archived. The registry is asked first; the workspace
|
|
268
292
|
* store file is the fallback, and an unreadable store simply means "none".
|
package/lib/index.js
CHANGED
|
@@ -1236,6 +1236,81 @@ export const applyPersonaMode = (assembly, { mode, deploymentPrefix }) => {
|
|
|
1236
1236
|
}
|
|
1237
1237
|
return changed ? { ...assembly, sections: next } : assembly;
|
|
1238
1238
|
};
|
|
1239
|
+
/**
|
|
1240
|
+
* Fold a persona into a prompt that one `complete: true` section owns.
|
|
1241
|
+
*
|
|
1242
|
+
* An agent preset may declare its own persona with `complete: true`; the registry then hands back that single
|
|
1243
|
+
* section as the whole system prompt, and it does so *after* the `system-prompt/assemble` waterfall — so no
|
|
1244
|
+
* listener can add a section of its own, and this plugin's section is discarded before it ever renders. The
|
|
1245
|
+
* only place a persona can still live is inside that section.
|
|
1246
|
+
* @param assembly - an assembled prompt.
|
|
1247
|
+
* @param persona - `{ text, mode }` of the persona that claims this assembly, or `undefined`.
|
|
1248
|
+
* @returns the assembly with the persona folded in, or the input when nothing applies.
|
|
1249
|
+
*/
|
|
1250
|
+
export const applyCompletePersona = (assembly, persona) => {
|
|
1251
|
+
const text = typeof persona?.text === 'string' ? persona.text.trim() : '';
|
|
1252
|
+
if (text === '')
|
|
1253
|
+
return assembly;
|
|
1254
|
+
const sections = assembly?.sections;
|
|
1255
|
+
if (!Array.isArray(sections) || sections.length !== 1)
|
|
1256
|
+
return assembly;
|
|
1257
|
+
const [only] = sections;
|
|
1258
|
+
// Our own section surviving means this is a normal prompt: the waterfall already placed the persona, and
|
|
1259
|
+
// folding it in again would print it twice.
|
|
1260
|
+
if (only?.name === SECTION_NAME)
|
|
1261
|
+
return assembly;
|
|
1262
|
+
const current = typeof only?.text === 'string' ? only.text : '';
|
|
1263
|
+
// `replace` takes the prompt over, which is the honest reading of "replace" when the preset owns every word;
|
|
1264
|
+
// `append` keeps that prompt and puts the persona right after it, where section order 1 would have been.
|
|
1265
|
+
// Trailing whitespace is dropped first so a complete prompt ending in a newline does not gain a blank line.
|
|
1266
|
+
const body = current.trimEnd();
|
|
1267
|
+
const next = persona.mode === 'replace' ? text : body === '' ? text : `${body}\n\n${text}`;
|
|
1268
|
+
return next === current ? assembly : { ...assembly, sections: [{ ...only, text: next }] };
|
|
1269
|
+
};
|
|
1270
|
+
/**
|
|
1271
|
+
* Reach the prompts a `complete: true` section owns.
|
|
1272
|
+
*
|
|
1273
|
+
* `assemble()` is wrapped instead of listening to the waterfall because the registry collapses the section
|
|
1274
|
+
* list *after* dispatching it: the waterfall's sections are overwritten by the complete one. Disposal restores
|
|
1275
|
+
* the original method, so unloading the plugin leaves no trace.
|
|
1276
|
+
* @param systemPrompt - the prompt registry service.
|
|
1277
|
+
* @param readPersona - resolves `{ text, mode }` for one assembly context, or `undefined`.
|
|
1278
|
+
* @param warn - diagnostic sink.
|
|
1279
|
+
* @returns the disposer that restores `assemble`.
|
|
1280
|
+
*/
|
|
1281
|
+
export const bridgeCompletePrompt = (systemPrompt, readPersona, warn = () => { }) => {
|
|
1282
|
+
const original = systemPrompt?.assemble;
|
|
1283
|
+
if (typeof original !== 'function')
|
|
1284
|
+
return () => { };
|
|
1285
|
+
// The wrapper is disposable state, not a permanent tombstone: after the plugin unloads it must stop folding
|
|
1286
|
+
// even when another plugin has wrapped `assemble` on top of it and the original method cannot be restored.
|
|
1287
|
+
let active = true;
|
|
1288
|
+
const patched = async (context) => {
|
|
1289
|
+
const assembly = await original.call(systemPrompt, context);
|
|
1290
|
+
if (!active)
|
|
1291
|
+
return assembly;
|
|
1292
|
+
try {
|
|
1293
|
+
return applyCompletePersona(assembly, readPersona(context));
|
|
1294
|
+
}
|
|
1295
|
+
catch (error) {
|
|
1296
|
+
warn('could not fold the persona into a complete system prompt', error);
|
|
1297
|
+
return assembly;
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
try {
|
|
1301
|
+
systemPrompt.assemble = patched;
|
|
1302
|
+
}
|
|
1303
|
+
catch (error) {
|
|
1304
|
+
// A host that freezes or seals the service must only cost this feature, never the whole plugin.
|
|
1305
|
+
warn('could not wrap assemble(); personas will not reach a complete prompt', error);
|
|
1306
|
+
return () => { };
|
|
1307
|
+
}
|
|
1308
|
+
return () => {
|
|
1309
|
+
active = false;
|
|
1310
|
+
if (systemPrompt.assemble === patched)
|
|
1311
|
+
systemPrompt.assemble = original;
|
|
1312
|
+
};
|
|
1313
|
+
};
|
|
1239
1314
|
/** Read one event's plain text out of the shapes DSH session logs use. */
|
|
1240
1315
|
const eventText = (event) => {
|
|
1241
1316
|
const parts = [];
|
|
@@ -1895,6 +1970,15 @@ export function apply(ctx, config = {}) {
|
|
|
1895
1970
|
catch (error) {
|
|
1896
1971
|
warn(`section "${SECTION_NAME}" is already registered; keeping the live one`, error);
|
|
1897
1972
|
}
|
|
1973
|
+
// A preset that owns the whole prompt (`complete: true`) never renders our section, so the persona has to be
|
|
1974
|
+
// folded into the section that preset does own. Registered through the effect, so unloading restores
|
|
1975
|
+
// `assemble` instead of leaving a wrapper behind.
|
|
1976
|
+
ctx.effect(() => bridgeCompletePrompt(ctx.systemPrompt, (context) => {
|
|
1977
|
+
const persona = resolvePersona(currentStore().personas, subjectOf(context?.agent)).persona;
|
|
1978
|
+
if (persona === undefined || persona.text.trim() === '')
|
|
1979
|
+
return undefined;
|
|
1980
|
+
return { text: sanitizePersona(persona.text, warn), mode: persona.mode };
|
|
1981
|
+
}, warn), 'agent-persona.complete-bridge');
|
|
1898
1982
|
// The store is this plugin's instruction source, so no tool call may write it. The guard is monotonic: once
|
|
1899
1983
|
// it refuses a call nothing later can allow it. Registered through the effect, so unloading the plugin
|
|
1900
1984
|
// removes it instead of leaving a process-wide refusal behind.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agent-persona",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Workspace personas for DeepSeek Harness (DSH): create many system-prompt personas and scope each one by workspace path or session id (exact / prefix / regex / contains), managed from a Web settings page with Markdown editing and AI tuning.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
package/scripts/install.ps1
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# pwsh -File scripts/install.ps1
|
|
4
4
|
# pwsh -File scripts/install.ps1 -Profile my-profile
|
|
5
5
|
# pwsh -File scripts/install.ps1 -Profile web -Target .
|
|
6
|
-
# pwsh -File scripts/install.ps1 -Profile web -Target dsh-agent-persona@0.
|
|
6
|
+
# pwsh -File scripts/install.ps1 -Profile web -Target dsh-agent-persona@0.2.1
|
|
7
7
|
param(
|
|
8
8
|
[string]$Profile = "web",
|
|
9
9
|
[string]$Target = "dsh-agent-persona"
|
|
@@ -58,14 +58,19 @@ Installed into profile "$Profile" (this checkout is v$Version).
|
|
|
58
58
|
|
|
59
59
|
dsh.profile.bundles is read at STARTUP, so restart the host now:
|
|
60
60
|
|
|
61
|
-
dsh web
|
|
61
|
+
dsh web # if you started the host by hand
|
|
62
|
+
# if a service runs it, restart that service instead; on Linux hosts that
|
|
63
|
+
# use systemd the unit is often named like dsh-web-<user>
|
|
62
64
|
|
|
63
65
|
Then check it loaded:
|
|
64
66
|
|
|
65
67
|
Get-Content "$State"
|
|
66
|
-
# 设置 -> Agent 人设
|
|
67
|
-
# 对话页顶部「人设」
|
|
68
|
-
#
|
|
68
|
+
# 设置 -> Agent 人设 完整管理页(可在插件页的「界面与功能」里隐藏这个入口)
|
|
69
|
+
# 对话页顶部「人设」 当前会话的人设 / 实际发送的提示词
|
|
70
|
+
# 插件 -> dsh-agent-persona 图标、README 和「界面与功能」开关;设置入口被隐藏后从这里回去
|
|
71
|
+
# dsh-better-sidebar 右侧 「Agent 人设」卡片(装了那个插件才有)
|
|
72
|
+
|
|
73
|
+
The switches on the plugin page and in Settings write the same prefs, so either place works.
|
|
69
74
|
|
|
70
75
|
Personas live in one file, shared by every profile unless you override config.storePath:
|
|
71
76
|
|
package/scripts/install.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# bash scripts/install.sh # published package, profile "web"
|
|
5
5
|
# bash scripts/install.sh my-profile # another profile
|
|
6
6
|
# bash scripts/install.sh web . # this checkout (development)
|
|
7
|
-
# bash scripts/install.sh web dsh-agent-persona@0.
|
|
7
|
+
# bash scripts/install.sh web dsh-agent-persona@0.2.1 # a pinned version
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
|
|
10
10
|
PROFILE="${1:-web}"
|
|
@@ -56,14 +56,18 @@ Installed into profile "${PROFILE}" (this checkout is v${VERSION}).
|
|
|
56
56
|
|
|
57
57
|
dsh.profile.bundles is read at STARTUP, so restart the host now:
|
|
58
58
|
|
|
59
|
-
dsh web
|
|
59
|
+
dsh web # if you started the host by hand
|
|
60
|
+
sudo systemctl restart dsh-web-wood # if systemd runs it (use your unit name)
|
|
60
61
|
|
|
61
62
|
Then check it loaded:
|
|
62
63
|
|
|
63
64
|
cat "${STATE}"
|
|
64
|
-
# 设置 → Agent 人设
|
|
65
|
-
# 对话页顶部「人设」
|
|
66
|
-
#
|
|
65
|
+
# 设置 → Agent 人设 完整管理页(可在插件页的「界面与功能」里隐藏这个入口)
|
|
66
|
+
# 对话页顶部「人设」 当前会话的人设 / 实际发送的提示词
|
|
67
|
+
# 插件 → dsh-agent-persona 图标、README 和「界面与功能」开关;设置入口被隐藏后从这里回去
|
|
68
|
+
# dsh-better-sidebar 右侧 「Agent 人设」卡片(装了那个插件才有)
|
|
69
|
+
|
|
70
|
+
The switches on the plugin page and in Settings write the same prefs, so either place works.
|
|
67
71
|
|
|
68
72
|
Personas live in one file, shared by every profile unless you override config.storePath:
|
|
69
73
|
|
package/src/client.ts
CHANGED
|
@@ -1001,7 +1001,13 @@ const readPrefs = () => {
|
|
|
1001
1001
|
* session it is showing; otherwise it keeps rendering the one it was first opened for.
|
|
1002
1002
|
*/
|
|
1003
1003
|
componentDidUpdate(previous) {
|
|
1004
|
-
if (this.sessionId() !== sessionIdOf(previous))
|
|
1004
|
+
if (this.sessionId() !== sessionIdOf(previous)) {
|
|
1005
|
+
void this.load()
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
1008
|
+
// A finished turn may have produced a new system prompt while the tab stayed open. Reload so
|
|
1009
|
+
// 「提示词」 is current; skip while the inline editor is open so typing is never clobbered.
|
|
1010
|
+
if (previous.running === true && this.props.running === false && this.state.editing !== true) void this.load()
|
|
1005
1011
|
}
|
|
1006
1012
|
|
|
1007
1013
|
sessionId() {
|
|
@@ -1287,6 +1293,16 @@ const readPrefs = () => {
|
|
|
1287
1293
|
}
|
|
1288
1294
|
|
|
1289
1295
|
|
|
1296
|
+
/**
|
|
1297
|
+
* The conversation tab receives the session snapshot hooks; the sidebar card does not. Subscribe
|
|
1298
|
+
* here so a turn that finishes while 「提示词」 is open can refresh itself instead of showing the
|
|
1299
|
+
* previous turn's prompt until the reader reloads the page.
|
|
1300
|
+
*/
|
|
1301
|
+
const PersonaViewLive = (props) => {
|
|
1302
|
+
const running = props.useSession((snapshot) => snapshot?.running === true)
|
|
1303
|
+
return h(PersonaView, { ...props, running })
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1290
1306
|
const dotState = (persona) => {
|
|
1291
1307
|
if (persona === undefined) return 'off'
|
|
1292
1308
|
if (!persona.enabled) return 'off'
|
|
@@ -3406,7 +3422,7 @@ const since = (timestamp) => {
|
|
|
3406
3422
|
id: 'agent-persona',
|
|
3407
3423
|
order: 30,
|
|
3408
3424
|
label: () => '人设',
|
|
3409
|
-
},
|
|
3425
|
+
}, PersonaViewLive))
|
|
3410
3426
|
}
|
|
3411
3427
|
const dropTabSeat = () => {
|
|
3412
3428
|
if (!tabSeatMounted) return
|
package/src/index.ts
CHANGED
|
@@ -1223,6 +1223,75 @@ export const applyPersonaMode = (assembly, { mode, deploymentPrefix }) => {
|
|
|
1223
1223
|
return changed ? { ...assembly, sections: next } : assembly
|
|
1224
1224
|
}
|
|
1225
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* Fold a persona into a prompt that one `complete: true` section owns.
|
|
1228
|
+
*
|
|
1229
|
+
* An agent preset may declare its own persona with `complete: true`; the registry then hands back that single
|
|
1230
|
+
* section as the whole system prompt, and it does so *after* the `system-prompt/assemble` waterfall — so no
|
|
1231
|
+
* listener can add a section of its own, and this plugin's section is discarded before it ever renders. The
|
|
1232
|
+
* only place a persona can still live is inside that section.
|
|
1233
|
+
* @param assembly - an assembled prompt.
|
|
1234
|
+
* @param persona - `{ text, mode }` of the persona that claims this assembly, or `undefined`.
|
|
1235
|
+
* @returns the assembly with the persona folded in, or the input when nothing applies.
|
|
1236
|
+
*/
|
|
1237
|
+
export const applyCompletePersona = (assembly, persona) => {
|
|
1238
|
+
const text = typeof persona?.text === 'string' ? persona.text.trim() : ''
|
|
1239
|
+
if (text === '') return assembly
|
|
1240
|
+
const sections = assembly?.sections
|
|
1241
|
+
if (!Array.isArray(sections) || sections.length !== 1) return assembly
|
|
1242
|
+
const [only] = sections
|
|
1243
|
+
// Our own section surviving means this is a normal prompt: the waterfall already placed the persona, and
|
|
1244
|
+
// folding it in again would print it twice.
|
|
1245
|
+
if (only?.name === SECTION_NAME) return assembly
|
|
1246
|
+
const current = typeof only?.text === 'string' ? only.text : ''
|
|
1247
|
+
// `replace` takes the prompt over, which is the honest reading of "replace" when the preset owns every word;
|
|
1248
|
+
// `append` keeps that prompt and puts the persona right after it, where section order 1 would have been.
|
|
1249
|
+
// Trailing whitespace is dropped first so a complete prompt ending in a newline does not gain a blank line.
|
|
1250
|
+
const body = current.trimEnd()
|
|
1251
|
+
const next = persona.mode === 'replace' ? text : body === '' ? text : `${body}\n\n${text}`
|
|
1252
|
+
return next === current ? assembly : { ...assembly, sections: [{ ...only, text: next }] }
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
/**
|
|
1256
|
+
* Reach the prompts a `complete: true` section owns.
|
|
1257
|
+
*
|
|
1258
|
+
* `assemble()` is wrapped instead of listening to the waterfall because the registry collapses the section
|
|
1259
|
+
* list *after* dispatching it: the waterfall's sections are overwritten by the complete one. Disposal restores
|
|
1260
|
+
* the original method, so unloading the plugin leaves no trace.
|
|
1261
|
+
* @param systemPrompt - the prompt registry service.
|
|
1262
|
+
* @param readPersona - resolves `{ text, mode }` for one assembly context, or `undefined`.
|
|
1263
|
+
* @param warn - diagnostic sink.
|
|
1264
|
+
* @returns the disposer that restores `assemble`.
|
|
1265
|
+
*/
|
|
1266
|
+
export const bridgeCompletePrompt = (systemPrompt, readPersona, warn = () => {}) => {
|
|
1267
|
+
const original = systemPrompt?.assemble
|
|
1268
|
+
if (typeof original !== 'function') return () => {}
|
|
1269
|
+
// The wrapper is disposable state, not a permanent tombstone: after the plugin unloads it must stop folding
|
|
1270
|
+
// even when another plugin has wrapped `assemble` on top of it and the original method cannot be restored.
|
|
1271
|
+
let active = true
|
|
1272
|
+
const patched = async (context) => {
|
|
1273
|
+
const assembly = await original.call(systemPrompt, context)
|
|
1274
|
+
if (!active) return assembly
|
|
1275
|
+
try {
|
|
1276
|
+
return applyCompletePersona(assembly, readPersona(context))
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
warn('could not fold the persona into a complete system prompt', error)
|
|
1279
|
+
return assembly
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
systemPrompt.assemble = patched
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
// A host that freezes or seals the service must only cost this feature, never the whole plugin.
|
|
1286
|
+
warn('could not wrap assemble(); personas will not reach a complete prompt', error)
|
|
1287
|
+
return () => {}
|
|
1288
|
+
}
|
|
1289
|
+
return () => {
|
|
1290
|
+
active = false
|
|
1291
|
+
if (systemPrompt.assemble === patched) systemPrompt.assemble = original
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1226
1295
|
/** Read one event's plain text out of the shapes DSH session logs use. */
|
|
1227
1296
|
const eventText = (event) => {
|
|
1228
1297
|
const parts = []
|
|
@@ -1858,6 +1927,15 @@ export function apply(ctx, config = {}) {
|
|
|
1858
1927
|
warn(`section "${SECTION_NAME}" is already registered; keeping the live one`, error)
|
|
1859
1928
|
}
|
|
1860
1929
|
|
|
1930
|
+
// A preset that owns the whole prompt (`complete: true`) never renders our section, so the persona has to be
|
|
1931
|
+
// folded into the section that preset does own. Registered through the effect, so unloading restores
|
|
1932
|
+
// `assemble` instead of leaving a wrapper behind.
|
|
1933
|
+
ctx.effect(() => bridgeCompletePrompt(ctx.systemPrompt, (context) => {
|
|
1934
|
+
const persona = resolvePersona(currentStore().personas, subjectOf(context?.agent)).persona
|
|
1935
|
+
if (persona === undefined || persona.text.trim() === '') return undefined
|
|
1936
|
+
return { text: sanitizePersona(persona.text, warn), mode: persona.mode }
|
|
1937
|
+
}, warn), 'agent-persona.complete-bridge')
|
|
1938
|
+
|
|
1861
1939
|
// The store is this plugin's instruction source, so no tool call may write it. The guard is monotonic: once
|
|
1862
1940
|
// it refuses a call nothing later can allow it. Registered through the effect, so unloading the plugin
|
|
1863
1941
|
// removes it instead of leaving a process-wide refusal behind.
|