@chaoset/adaptive-perf 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-plugins contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # @chaoset/adaptive-perf — DSH 极简性能自适应插件
2
+
3
+ 让 **标准模式(standard)** 与 **PTC 模式(code)** 达到 **极简模式(minimal)**
4
+ 级别的高性能,同时保留完整能力——不是静态裁剪,而是**动态自适应**:
5
+
6
+ - **运行时上下文抑制**:对目标 preset 的会话调用 `suppressRuntimeContext()`
7
+ (与极简模式的 `includeRuntimeContext: false` 同一机制),每次模型请求省掉
8
+ "Current runtime context" 快照文本,**零功能损失**。
9
+ - **工具目录自适应精简**:会话启动时按"工具族"默认隐藏高开销低频工具
10
+ (子代理 / 工作流 / ralph / goal 等编排类),只保留核心编码工具
11
+ (bash / 文件读写编辑 / 检索 / jobs / todo / 提问 / 网页搜索 / 技能 / 计划)。
12
+ 标准模式的工具目录因此逼近极简;PTC 模式系统提示里的 **SDK 参考段同步缩小**。
13
+ - **需求信号自动放行**(会话内单调升级,只升不降,限制请求缓存失效次数):
14
+ - 关键词信号:用户消息命中工具族触发词(如"子代理"、"工作流")→ 放行该族;
15
+ - 失败信号:PTC 程序调用被隐藏工具报 `UNKNOWN_TOOL` → 放行该族,下次即可调用。
16
+
17
+ 所有开关与工具族可在设置页配置、随时放行;配置保存后**热生效**(无需重启)。
18
+
19
+ ## 为什么极简模式最快(源码对照结论)
20
+
21
+ | 维度 | 极简模式 | 标准 / PTC 模式 | 本插件做法 |
22
+ |---|---|---|---|
23
+ | 运行时上下文快照 | 关闭(`includeRuntimeContext: false`) | 每次请求注入文件策略/审批策略等快照 | 同一机制:`suppressRuntimeContext()` |
24
+ | 模型可见工具目录 | 2 个(bash、str_replace_editor) | ~22 个工具 schema 全部进请求 | 默认隐藏 4 个编排类工具族(~11 个工具),按需放行 |
25
+ | PTC SDK 参考段 | 无 | 全部工具的类型+描述渲染进提示 | 随目录缩小(同一 `visible` 视图驱动) |
26
+
27
+ ## 安装(npm 生态方式)
28
+
29
+ 本包是 DSH bundle:自带 `cordis.patch.yml`,使用 `dsh plugin` 安装后会
30
+ 自动加入 profile 的 `dsh.profile.bundles`,无需手改 `cordis.patch.yml`。
31
+
32
+ > 给 AI agent:直接执行下面的 `dsh plugin ...` 命令即可,不要手动编辑 `cordis.patch.yml`。
33
+
34
+ ```bash
35
+ # 安装到默认 web profile
36
+ dsh plugin --profile web add @chaoset/adaptive-perf
37
+
38
+ # 指定其他 profile
39
+ dsh plugin --profile tui add @chaoset/adaptive-perf
40
+
41
+ # 卸载
42
+ dsh plugin --profile web remove @chaoset/adaptive-perf
43
+ ```
44
+
45
+ 也可以从其他来源安装:
46
+
47
+ - **GitHub**:如果该包有独立 Git 仓库或 Release tarball,可用
48
+ `dsh plugin --profile web add github:owner/repo` 或
49
+ `dsh plugin --profile web add https://github.com/owner/repo/releases/download/v1.0.0/adaptive-perf.tgz`
50
+ - **镜像站**:`dsh plugin --profile web add @chaoset/adaptive-perf --registry=https://registry.npmmirror.com`
51
+
52
+ 更完整的说明见仓库根目录 `README.md` 的「安装来源」。
53
+
54
+ 安装后**重启 harness** 生效(或等待 DSH 对配置层变更的响应)。
55
+
56
+ ## 配置
57
+
58
+ 生效顺序(后者覆盖前者):
59
+
60
+ 1. 插件内置默认值
61
+ 2. bundle patch 中 `cordis.patch.yml` 的 config
62
+ 3. 用户 profile/home 的 `cordis.patch.yml` 覆盖
63
+ 4. **DSH 设置页 → 插件配置 → 极简性能自适应**(保存到
64
+ `$DSH_HOME/plugins/adaptive-perf/config.json`,立即热生效)
65
+
66
+ | 字段 | 默认值 | 说明 |
67
+ |---|---|---|
68
+ | `enabled` | `true` | 总开关 |
69
+ | `presets` | `["standard", "code"]` | 应用自适应的 preset id |
70
+ | `suppressRuntimeContext` | `true` | 抑制运行时上下文快照(零功能损失) |
71
+ | `leanByDefault` | `true` | 会话启动即隐藏编排类工具族 |
72
+ | `escalateOnKeyword` | `true` | 用户消息命中触发词 → 放行该族 |
73
+ | `escalateOnUnknownTool` | `true` | 工具调用失败(UNKNOWN_TOOL)→ 放行该族 |
74
+ | `coreTools` | 核心编码工具 | 展示用:不进入任何限制族 |
75
+ | `families` | 见下 | 工具族:`{ 族名: { enabled, tools, keywords } }` |
76
+
77
+ 默认工具族(可整族禁用、改名、增删触发词):
78
+
79
+ ```json
80
+ {
81
+ "delegation": { "tools": ["subagent", "subagent_fork", "send_message", "list_agents", "interrupt_agent"], "keywords": ["子代理", "子agent", "委托", "分派", "subagent", "delegate"] },
82
+ "workflow": { "tools": ["workflow"], "keywords": ["工作流", "workflow", "编排"] },
83
+ "ralph": { "tools": ["ralph"], "keywords": ["ralph"] },
84
+ "goal": { "tools": ["create_goal", "get_goal", "update_goal"], "keywords": ["长期目标", "跨轮次", "goal"] }
85
+ }
86
+ ```
87
+
88
+ > 工具名只在目标 preset 目录中**实际存在**时才被隐藏(运行时取交集),preset
89
+ > 升级改名/删除工具不会导致插件报错;`tools.restrict()` 的限制按 agent 作用域
90
+ > 生效,子代理同样遵循自适应策略(其自身注册的委托机制工具不受影响)。
91
+
92
+ ## 实现要点
93
+
94
+ - 监听 host 根作用域的 `agent/created` / `agent/disposed` / `agent/inbox/inserted`
95
+ / `tools/result`(与 `dsh-agent-presets` 自身同款事件用法),按
96
+ `agentPresets.composedPreset(agent.ctx)` 识别目标 preset
97
+ - 限制通过 `agent.ctx.tools.restrict({ deny })` 逐族注册:多族限制自动取交集,
98
+ 升级 = 释放该族 disposer(单会话单调、一次性目录变化,缓存失效有界)
99
+ - 会话启动/插件重载时用 `tools.schemas(agent)` 记录可见目录,限制与之取交集
100
+ - 全部副作用(抑制器、限制、监听)随 agent 销毁或插件卸载自动释放
101
+ - **fail-safe**:插件任何内部错误只记日志,不拖垮 harness
@@ -0,0 +1,318 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@chaoset/adaptive-perf",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ let react = require("react");
7
+
8
+ // ── 样式(注入 style 标签,模仿官方卡片外观)──────────────────────────
9
+ var css = ".ap_card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;overflow:hidden}.ap_card[data-open=true]{border-color:var(--dsw-alias-border-l1);box-shadow:var(--dsw-shadow-lv1)}.ap_header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:10px;align-items:center;gap:12px;padding:12px 14px;display:flex}.ap_header:hover,.ap_card[data-open=true]>.ap_header{background:var(--dsw-alias-interactive-bg-hover)}.ap_title{flex:1;min-width:0;font-size:14px;font-weight:600;line-height:20px}.ap_badge{white-space:nowrap;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:16px}.ap_body{border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);padding:10px 14px 12px}.ap_field{flex-direction:column;gap:4px;padding:8px 0;display:flex}.ap_label{font-size:12px;font-weight:500;line-height:18px}.ap_input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:32px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 10px;font-size:13px;line-height:20px}.ap_textarea{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 10px;font-size:12px;line-height:18px;resize:vertical;min-height:120px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.ap_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:11px;line-height:16px}.ap_footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:8px 0 2px;display:flex}.ap_save{font:inherit;cursor:pointer;border:1px solid transparent;border-radius:6px;padding:4px 14px;font-size:12px;line-height:18px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}.ap_discard{font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:4px 14px;font-size:12px;line-height:18px;background:0 0;color:inherit}.ap_status{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary);flex:1}.ap_status.ap_error{color:var(--dsw-alias-state-danger-primary)}";
10
+ var tagId = "@chaoset/adaptive-perf/client.css";
11
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
12
+ var tag = document.createElement("style");
13
+ tag.dataset.plugin = "@chaoset/adaptive-perf";
14
+ tag.dataset.pluginCss = tagId;
15
+ tag.textContent = css;
16
+ document.head.appendChild(tag);
17
+ }
18
+
19
+ // ── 字典 ─────────────────────────────────────────────────────────────
20
+ const NS = "settings.plugins.adaptivePerf";
21
+ const zh = {
22
+ title: "极简性能自适应(adaptive-perf)",
23
+ hint: "让标准模式 / PTC 模式达到极简模式级别的高性能:抑制运行时上下文快照,启动即精简工具目录,按需求信号自动放行工具族。",
24
+ unsaved: "有未保存的修改",
25
+ discard: "放弃修改",
26
+ saving: "保存中…",
27
+ save: "保存",
28
+ saveFailed: "保存失败",
29
+ saved: "已保存,热生效(新会话立即生效)",
30
+ loadFailed: "读取配置失败",
31
+ invalidFamilies: "families 必须是合法 JSON 对象",
32
+ enabled: "启用插件",
33
+ presets: "目标 preset",
34
+ fieldPresets: "逗号分隔的 preset id(如 standard, code)",
35
+ suppressRuntimeContext: "抑制运行时上下文快照",
36
+ fieldSuppressRuntimeContext: "等同极简模式的 includeRuntimeContext: false,每次请求省掉快照文本(零功能损失)",
37
+ leanByDefault: "启动即精简工具目录",
38
+ fieldLeanByDefault: "默认隐藏编排类工具族(子代理/工作流/ralph/goal),核心编码工具保留",
39
+ escalateOnKeyword: "关键词自动放行",
40
+ fieldEscalateOnKeyword: "用户消息命中工具族触发词(如“子代理”)时,该会话放行对应工具族",
41
+ escalateOnUnknownTool: "失败自动放行",
42
+ fieldEscalateOnUnknownTool: "PTC 程序调用被隐藏工具报 UNKNOWN_TOOL 时自动放行对应族",
43
+ families: "工具族(高级)",
44
+ fieldFamilies: "JSON:{ 族名: { enabled, tools: [工具名], keywords: [触发词] } }。保存后热生效。",
45
+ coreTools: "核心工具(永不隐藏)",
46
+ fieldCoreTools: "展示用:这些工具不进入任何限制族。未列入任何族的工具也默认保留。"
47
+ };
48
+ const en = {
49
+ title: "Adaptive performance (adaptive-perf)",
50
+ hint: "Bring standard / PTC presets to minimal-mode-level performance: suppress the runtime-context snapshot, start with a lean tool catalog, and re-enable tool families on demand.",
51
+ unsaved: "Unsaved changes",
52
+ discard: "Discard",
53
+ saving: "Saving…",
54
+ save: "Save",
55
+ saveFailed: "Save failed",
56
+ saved: "Saved; hot-applied (new sessions take effect immediately)",
57
+ loadFailed: "Failed to load config",
58
+ invalidFamilies: "families must be valid JSON object",
59
+ enabled: "Enable plugin",
60
+ presets: "Target presets",
61
+ fieldPresets: "Comma-separated preset ids (e.g. standard, code)",
62
+ suppressRuntimeContext: "Suppress runtime-context snapshot",
63
+ fieldSuppressRuntimeContext: "Same as minimal's includeRuntimeContext: false; drops the snapshot text from every request (zero feature loss)",
64
+ leanByDefault: "Lean tool catalog at session start",
65
+ fieldLeanByDefault: "Hide orchestration families (subagent/workflow/ralph/goal) by default; core coding tools stay",
66
+ escalateOnKeyword: "Keyword escalation",
67
+ fieldEscalateOnKeyword: "A user message matching a family's trigger words re-enables that family for the session",
68
+ escalateOnUnknownTool: "Failure escalation",
69
+ fieldEscalateOnUnknownTool: "A PTC program calling a hidden tool (UNKNOWN_TOOL) re-enables that family",
70
+ families: "Tool families (advanced)",
71
+ fieldFamilies: "JSON: { familyId: { enabled, tools: [names], keywords: [triggers] } }. Hot-applied on save.",
72
+ coreTools: "Core tools (never hidden)",
73
+ fieldCoreTools: "Informational: these never enter any restriction family. Tools not listed in any family stay visible too."
74
+ };
75
+
76
+ // ── 表单字段 ─────────────────────────────────────────────────────────
77
+ function Field(props) {
78
+ return react.createElement(
79
+ "label",
80
+ { className: "ap_field" },
81
+ react.createElement("span", { className: "ap_label" }, props.label),
82
+ props.children,
83
+ props.hint !== void 0 ? react.createElement("p", { className: "ap_hint" }, props.hint) : null
84
+ );
85
+ }
86
+ function TextField(props) {
87
+ return react.createElement(Field, { label: props.label, hint: props.hint },
88
+ react.createElement("input", {
89
+ className: "ap_input",
90
+ value: props.value,
91
+ onChange: (e) => props.onChange(e.target.value),
92
+ disabled: props.disabled === true
93
+ })
94
+ );
95
+ }
96
+ function BoolField(props) {
97
+ return react.createElement(Field, { label: props.label, hint: props.hint },
98
+ react.createElement("input", {
99
+ type: "checkbox",
100
+ checked: props.value === true,
101
+ onChange: (e) => props.onChange(e.target.checked),
102
+ disabled: props.disabled === true
103
+ })
104
+ );
105
+ }
106
+ function familiesToText(families) {
107
+ return JSON.stringify(families, null, 2);
108
+ }
109
+ function textToFamilies(text) {
110
+ const parsed = JSON.parse(text);
111
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
112
+ throw new TypeError("families must be a JSON object");
113
+ }
114
+ for (const [id, fam] of Object.entries(parsed)) {
115
+ if (fam === null || typeof fam !== "object" || Array.isArray(fam)) {
116
+ throw new TypeError("family \"" + id + "\" must be an object");
117
+ }
118
+ if (fam.enabled !== void 0 && typeof fam.enabled !== "boolean") {
119
+ throw new TypeError("family \"" + id + "\".enabled must be a boolean");
120
+ }
121
+ for (const key of ["tools", "keywords"]) {
122
+ if (fam[key] !== void 0 && !(Array.isArray(fam[key]) && fam[key].every((v) => typeof v === "string"))) {
123
+ throw new TypeError("family \"" + id + "\"." + key + " must be an array of strings");
124
+ }
125
+ }
126
+ }
127
+ return parsed;
128
+ }
129
+
130
+ // ── 配置卡片 ─────────────────────────────────────────────────────────
131
+ function AdaptivePerfCard(props) {
132
+ const t = props.t;
133
+ const [open, setOpen] = react.useState(false);
134
+ const [cfg, setCfg] = react.useState(null);
135
+ const [draft, setDraft] = react.useState(null);
136
+ const [familiesText, setFamiliesText] = react.useState("");
137
+ const [saving, setSaving] = react.useState(false);
138
+ const [status, setStatus] = react.useState(null);
139
+
140
+ react.useEffect(() => {
141
+ let cancelled = false;
142
+ props.getConfig().then((value) => {
143
+ if (cancelled) return;
144
+ setCfg(value);
145
+ setDraft(value);
146
+ setFamiliesText(familiesToText(value.families));
147
+ }).catch((error) => {
148
+ if (!cancelled) setStatus({ kind: "error", text: t("loadFailed") + ": " + (error && error.message || error) });
149
+ });
150
+ return () => { cancelled = true; };
151
+ }, []);
152
+
153
+ let familiesInvalid = false;
154
+ let familiesError = null;
155
+ if (draft !== null) {
156
+ try {
157
+ textToFamilies(familiesText);
158
+ } catch (error) {
159
+ familiesInvalid = true;
160
+ familiesError = error && error.message || String(error);
161
+ }
162
+ }
163
+ const dirty = draft !== null && (JSON.stringify(draft) !== JSON.stringify(cfg) || familiesInvalid === false && familiesToText(draft.families) !== familiesText);
164
+ const invalid = draft !== null && familiesInvalid;
165
+ const discard = () => {
166
+ setDraft(cfg);
167
+ setFamiliesText(cfg === null ? "" : familiesToText(cfg.families));
168
+ setStatus(null);
169
+ };
170
+ const save = () => {
171
+ if (draft === null || familiesInvalid) return;
172
+ let next = draft;
173
+ try {
174
+ next = { ...draft, families: textToFamilies(familiesText) };
175
+ } catch (error) {
176
+ setStatus({ kind: "error", text: t("invalidFamilies") + ": " + (error && error.message || error) });
177
+ return;
178
+ }
179
+ setSaving(true);
180
+ setStatus(null);
181
+ props.setConfig(next).then(() => {
182
+ setCfg(next);
183
+ setDraft(next);
184
+ setFamiliesText(familiesToText(next.families));
185
+ setStatus({ kind: "ok", text: t("saved") });
186
+ }).catch((error) => {
187
+ setStatus({ kind: "error", text: t("saveFailed") + ": " + (error && error.message || error) });
188
+ }).finally(() => setSaving(false));
189
+ };
190
+
191
+ return react.createElement(
192
+ "li",
193
+ { className: "ap_card", "data-open": open },
194
+ react.createElement(
195
+ "button",
196
+ {
197
+ className: "ap_header",
198
+ type: "button",
199
+ onClick: () => setOpen(!open),
200
+ "aria-expanded": open
201
+ },
202
+ react.createElement("span", { className: "ap_title" }, t("title")),
203
+ dirty ? react.createElement("span", { className: "ap_badge" }, t("unsaved")) : null,
204
+ react.createElement("span", null, open ? "▲" : "▼")
205
+ ),
206
+ open ? react.createElement(
207
+ "div",
208
+ { className: "ap_body" },
209
+ react.createElement("p", { className: "ap_hint" }, t("hint")),
210
+ react.createElement(BoolField, {
211
+ label: t("enabled"),
212
+ value: draft === null ? false : draft.enabled,
213
+ disabled: draft === null,
214
+ onChange: (v) => setDraft({ ...draft, enabled: v })
215
+ }),
216
+ react.createElement(TextField, {
217
+ label: t("presets"),
218
+ hint: t("fieldPresets"),
219
+ value: draft === null ? "" : (draft.presets || []).join(", "),
220
+ disabled: draft === null,
221
+ onChange: (v) => setDraft({ ...draft, presets: v.split(/[,,\s]+/).filter((x) => x.length > 0) })
222
+ }),
223
+ react.createElement(BoolField, {
224
+ label: t("suppressRuntimeContext"),
225
+ hint: t("fieldSuppressRuntimeContext"),
226
+ value: draft === null ? false : draft.suppressRuntimeContext,
227
+ disabled: draft === null,
228
+ onChange: (v) => setDraft({ ...draft, suppressRuntimeContext: v })
229
+ }),
230
+ react.createElement(BoolField, {
231
+ label: t("leanByDefault"),
232
+ hint: t("fieldLeanByDefault"),
233
+ value: draft === null ? false : draft.leanByDefault,
234
+ disabled: draft === null,
235
+ onChange: (v) => setDraft({ ...draft, leanByDefault: v })
236
+ }),
237
+ react.createElement(BoolField, {
238
+ label: t("escalateOnKeyword"),
239
+ hint: t("fieldEscalateOnKeyword"),
240
+ value: draft === null ? false : draft.escalateOnKeyword,
241
+ disabled: draft === null,
242
+ onChange: (v) => setDraft({ ...draft, escalateOnKeyword: v })
243
+ }),
244
+ react.createElement(BoolField, {
245
+ label: t("escalateOnUnknownTool"),
246
+ hint: t("fieldEscalateOnUnknownTool"),
247
+ value: draft === null ? false : draft.escalateOnUnknownTool,
248
+ disabled: draft === null,
249
+ onChange: (v) => setDraft({ ...draft, escalateOnUnknownTool: v })
250
+ }),
251
+ react.createElement(Field, {
252
+ label: t("families"),
253
+ hint: familiesInvalid ? t("invalidFamilies") + (familiesError !== null ? " — " + familiesError : "") : t("fieldFamilies")
254
+ },
255
+ react.createElement("textarea", {
256
+ className: "ap_textarea",
257
+ value: familiesText,
258
+ disabled: draft === null,
259
+ spellCheck: false,
260
+ onChange: (e) => setFamiliesText(e.target.value)
261
+ })
262
+ ),
263
+ react.createElement(Field, {
264
+ label: t("coreTools"),
265
+ hint: t("fieldCoreTools")
266
+ },
267
+ react.createElement("p", { className: "ap_hint" }, draft === null ? "" : (draft.coreTools || []).join(", "))
268
+ ),
269
+ react.createElement(
270
+ "div",
271
+ { className: "ap_footer" },
272
+ react.createElement(
273
+ "span",
274
+ { className: "ap_status" + (status !== null && status.kind === "error" ? " ap_error" : ""), role: "status" },
275
+ status !== null ? status.text : ""
276
+ ),
277
+ react.createElement(
278
+ "button",
279
+ { className: "ap_discard", type: "button", disabled: saving || draft === null || !dirty, onClick: discard },
280
+ t("discard")
281
+ ),
282
+ react.createElement(
283
+ "button",
284
+ { className: "ap_save", type: "button", disabled: saving || draft === null || invalid || !dirty, onClick: save },
285
+ saving ? t("saving") : t("save")
286
+ )
287
+ )
288
+ ) : null
289
+ );
290
+ }
291
+
292
+ // ── 插件 apply ───────────────────────────────────────────────────────
293
+ const inject = ["slots", "locale", "remote", "remote.adaptivePerfConfig"];
294
+ function apply(ctx) {
295
+ const t = ctx.locale.bind(NS);
296
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "adaptive-perf: dictionaries");
297
+ const getConfig = () => ctx.remote.adaptivePerfConfig.get().then((result) => {
298
+ if (!result.ok) throw new Error("adaptivePerfConfig.get failed: " + result.error.code + ": " + result.error.message);
299
+ return result.value.config;
300
+ });
301
+ const setConfig = (partial) => ctx.remote.adaptivePerfConfig.set(partial).then((result) => {
302
+ if (!result.ok) throw new Error("adaptivePerfConfig.set failed: " + result.error.code + ": " + result.error.message);
303
+ return result.value;
304
+ });
305
+ ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
306
+ name: "settings.plugin.item",
307
+ id: "adaptive-perf",
308
+ order: 40,
309
+ locale: NS,
310
+ inject: () => ({ getConfig, setConfig })
311
+ }, AdaptivePerfCard));
312
+ }
313
+
314
+ exports.apply = apply;
315
+ exports.inject = inject;
316
+ return module.exports;
317
+ }
318
+ });
@@ -0,0 +1,17 @@
1
+ # @chaoset/adaptive-perf bundle patch
2
+ #
3
+ # Installed automatically by:
4
+ # dsh plugin --profile <name> add @chaoset/adaptive-perf
5
+ #
6
+ # This patch inserts the host plugin row with the default adaptive behavior.
7
+
8
+ - insert:
9
+ - id: adaptive-perf
10
+ name: '@chaoset/adaptive-perf'
11
+ config:
12
+ enabled: true
13
+ presets: [standard, code]
14
+ suppressRuntimeContext: true
15
+ leanByDefault: true
16
+ escalateOnKeyword: true
17
+ escalateOnUnknownTool: true
@@ -0,0 +1,97 @@
1
+ /**
2
+ * config-store — 插件配置持久化
3
+ *
4
+ * 配置文件位于 $DSH_HOME/plugins/<name>/config.json(默认 ~/.dsh,与
5
+ * file:// 部署模式共用)。生效顺序(后者覆盖前者):
6
+ * 1. 插件内置默认值(DEFAULT_CONFIG)
7
+ * 2. cordis.patch.yml 传入的 config(安装时生成的默认块)
8
+ * 3. config.json(设置页 UI 保存,权威)
9
+ *
10
+ * 保存后通过 onUpdate 回调立即热更新运行中的配置(包装闭包读共享 state)。
11
+ * 写入采用随机临时文件 + fsync + rename,权限 0600;损坏 JSON 只告警并回退。
12
+ */
13
+
14
+ import { randomUUID } from "node:crypto";
15
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { homedir } from "node:os";
18
+
19
+ /**
20
+ * 创建配置存储。
21
+ * @param options - { name, defaults, patchConfig, onUpdate,
22
+ * validate?: (partial) => void, warn?: (message) => void }
23
+ */
24
+ export function createConfigStore(options) {
25
+ // 与 harness 的 DSH_HOME 约定保持一致:默认 ~/.dsh,可用 $DSH_HOME 覆盖。
26
+ const dshHome = process.env.DSH_HOME?.trim() ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh");
27
+ const file = join(dshHome, "plugins", options.name, "config.json");
28
+ const warn = options.warn ?? (() => {});
29
+ let readWarningShown = false;
30
+
31
+ function readJson() {
32
+ try {
33
+ if (!existsSync(file)) return {};
34
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
35
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
36
+ readWarningShown = false;
37
+ return parsed;
38
+ }
39
+ if (!readWarningShown) {
40
+ readWarningShown = true;
41
+ warn(`config file ${file} must contain a JSON object; using empty config`);
42
+ }
43
+ return {};
44
+ } catch (error) {
45
+ if (!readWarningShown) {
46
+ readWarningShown = true;
47
+ warn(`failed to read config file ${file}: ${error?.message || error}; using empty config`);
48
+ }
49
+ return {};
50
+ }
51
+ }
52
+
53
+ /** 当前生效配置(默认 + patch + json 合并)。 */
54
+ function effective() {
55
+ return { ...options.defaults, ...options.patchConfig, ...readJson() };
56
+ }
57
+
58
+ /** 保存部分配置到 config.json 并触发热更新,返回新的生效配置。 */
59
+ function set(partial) {
60
+ if (partial === null || typeof partial !== "object" || Array.isArray(partial)) {
61
+ throw new TypeError("set expects a plain config object");
62
+ }
63
+ options.validate?.(partial);
64
+ const merged = { ...readJson(), ...partial };
65
+ mkdirSync(dirname(file), { recursive: true });
66
+
67
+ const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
68
+ let fd;
69
+ try {
70
+ fd = openSync(tmp, "w", 0o600);
71
+ writeFileSync(fd, JSON.stringify(merged, null, 2) + "\n", "utf8");
72
+ fsyncSync(fd);
73
+ closeSync(fd);
74
+ fd = void 0;
75
+ renameSync(tmp, file);
76
+ // POSIX 目录项持久化(Windows 上目录句柄不可打开时忽略)。
77
+ try {
78
+ const dir = openSync(dirname(file), "r");
79
+ try { fsyncSync(dir); } finally { closeSync(dir); }
80
+ } catch {}
81
+ } catch (error) {
82
+ if (fd !== void 0) try { closeSync(fd); } catch {}
83
+ try { unlinkSync(tmp); } catch {}
84
+ throw error;
85
+ }
86
+
87
+ const next = { ...options.defaults, ...options.patchConfig, ...merged };
88
+ try {
89
+ options.onUpdate?.(merged, next);
90
+ } catch (error) {
91
+ warn(`onUpdate failed after config save: ${error?.message || error}`);
92
+ }
93
+ return next;
94
+ }
95
+
96
+ return { file, effective, set };
97
+ }
package/lib/index.mjs ADDED
@@ -0,0 +1,481 @@
1
+ /**
2
+ * adaptive-perf — 极简性能自适应插件(@chaoset/adaptive-perf)
3
+ *
4
+ * 目标:让「标准模式(standard)」与「PTC 模式(code)」达到与「极简模式
5
+ * (minimal)」相同级别的高性能。极简模式快的原因(对源码的对照结论):
6
+ * 1. persona 配置了 includeRuntimeContext: false —— 每次模型请求不注入
7
+ * "Current runtime context" 快照(文件策略 / 审批策略等动态段);
8
+ * 2. 工具目录极小(只有 bash + str_replace_editor)—— 每个请求的
9
+ * tool schema 与(PTC 的)SDK 参考段都随之变小;
10
+ * 3. persona 为 complete 固定提示,无其它全局 prompt 段。
11
+ *
12
+ * 本插件在两个维度上做"动态自适应"(而不是静态裁剪):
13
+ * A. 运行时上下文抑制:对目标 preset 的会话调用
14
+ * agent.ctx.systemPrompt.suppressRuntimeContext()(与极简模式同一机制),
15
+ * 每次请求省掉快照文本,零功能损失。
16
+ * B. 工具目录自适应精简:会话启动时按"工具族"默认隐藏高开销低频工具
17
+ * (子代理 / 工作流 / ralph / goal 等编排类),只保留核心编码工具,
18
+ * 让标准模式的目录逼近极简、PTC 的 SDK 段同步缩小;随后根据两类信号
19
+ * 在会话内"单调升级"放行对应工具族(只升不降,限制请求缓存失效次数):
20
+ * - 关键词信号:用户消息命中某工具族的触发词(如"子代理")→ 放行该族;
21
+ * - 失败信号:PTC 的 run_code 程序调用被隐藏工具报 UNKNOWN_TOOL
22
+ * (tools/result 失败文本含工具名)→ 放行该族,下次程序即可调用。
23
+ *
24
+ * 实现全部使用 dsh 公开服务契约(Inspect 确认):
25
+ * - ctx.agentPresets.composedPreset(agent.ctx) 读取会话所属 preset
26
+ * - agent.ctx.systemPrompt.suppressRuntimeContext() 抑制上下文快照
27
+ * - agent.ctx.tools.schemas(agent) 读取该 agent 当前可见工具(限制交集)
28
+ * - agent.ctx.tools.restrict({ deny }) 按 agent 作用域裁剪继承工具
29
+ * - agent/created、agent/disposed、agent/inbox/inserted、tools/result 事件
30
+ * 监听器都注册在 host 根作用域(与 dsh-agent-presets 自身同款用法),事件按
31
+ * scope 过滤分发,子作用域派发的事件根监听器可收到。
32
+ *
33
+ * 配置:cordis.patch.yml 的 config(安装默认)与
34
+ * ~/.dsh/plugins/adaptive-perf/config.json(设置页 UI,权威)合并,
35
+ * 保存后热生效(新会话立即生效;已运行会话按新配置重算限制)。
36
+ *
37
+ * 本文件不依赖任何 dsh 内部包(纯 ESM + ctx 服务),可独立安装。
38
+ */
39
+
40
+ import { createConfigStore } from './config-store.mjs';
41
+
42
+ // remote 服务(设置页 UI 的配置读写)可选:typert-protocol 不可用时
43
+ // 动态 import 失败,核心功能(自适应)照常工作。
44
+ let PluginConfigGateway = null;
45
+ try {
46
+ ({ PluginConfigGateway } = await import('./remote.mjs'));
47
+ } catch (error) {
48
+ console.warn('adaptive-perf: settings gateway unavailable: ' + (error && error.message || error));
49
+ }
50
+
51
+ export const name = 'adaptive-perf';
52
+
53
+ /**
54
+ * 默认配置。apply 时与 YAML 传入的 config 合并(cordis 不合并小写 config 导出)。
55
+ *
56
+ * families:工具族。每族可配 tools(该族工具名,需存在于目标 preset 的目录,
57
+ * 插件运行时按实际可见工具取交集,不会因改名/缺失而崩溃)与 keywords(用户消息
58
+ * 命中任一触发词即放行该族;大小写不敏感,子串匹配)。
59
+ */
60
+ export const DEFAULT_CONFIG = {
61
+ /** 总开关:关闭后插件对会话不产生任何影响。 */
62
+ enabled: true,
63
+ /** 应用自适应的 agent preset 列表(composedPreset 返回值)。 */
64
+ presets: ['standard', 'code'],
65
+ /** 抑制运行时上下文快照(等同极简模式的 includeRuntimeContext: false)。 */
66
+ suppressRuntimeContext: true,
67
+ /** 会话启动时默认隐藏编排类工具族(核心编码工具始终保留)。 */
68
+ leanByDefault: true,
69
+ /** 用户消息命中工具族触发词时自动放行该族(会话内单调)。 */
70
+ escalateOnKeyword: true,
71
+ /** 工具调用失败(UNKNOWN_TOOL 文本含隐藏工具名)时自动放行该族。 */
72
+ escalateOnUnknownTool: true,
73
+ /** 核心编码工具(不进入任何限制族;此处仅作文档展示)。 */
74
+ coreTools: [
75
+ 'bash', 'read', 'write', 'edit', 'glob', 'grep', 'read_image',
76
+ 'job_output', 'job_list', 'job_kill',
77
+ 'todo_write', 'ask_user_question', 'web_search', 'skill', 'exit_plan_mode',
78
+ ],
79
+ /** 默认隐藏、按需放行的工具族。 */
80
+ families: {
81
+ delegation: {
82
+ enabled: true,
83
+ tools: ['subagent', 'subagent_fork', 'send_message', 'list_agents', 'interrupt_agent'],
84
+ keywords: ['子代理', '子agent', '委托', '分派', '派给', '子任务', 'subagent', 'delegate', 'fork'],
85
+ },
86
+ workflow: {
87
+ enabled: true,
88
+ tools: ['workflow'],
89
+ keywords: ['工作流', 'workflow', '编排', '多阶段'],
90
+ },
91
+ ralph: {
92
+ enabled: true,
93
+ tools: ['ralph'],
94
+ keywords: ['ralph', '全新agent', 'fresh agent'],
95
+ },
96
+ goal: {
97
+ enabled: true,
98
+ tools: ['create_goal', 'get_goal', 'update_goal'],
99
+ keywords: ['长期目标', '跨轮次', '目标追踪', 'goal'],
100
+ },
101
+ },
102
+ };
103
+
104
+ /** 轻量配置校验/归一化:非法字段回退默认值,避免 config.json 或 remote.set
105
+ * 写入错误类型后在事件路径上抛 TypeError。 */
106
+ function boolValue(value, fallback) {
107
+ return typeof value === 'boolean' ? value : fallback;
108
+ }
109
+
110
+ function stringList(value, fallback) {
111
+ if (Array.isArray(value)) {
112
+ const out = value.filter((v) => typeof v === 'string' && v.trim().length > 0).map((v) => v.trim());
113
+ return out.length > 0 ? out : fallback;
114
+ }
115
+ if (typeof value === 'string') {
116
+ const out = value.split(/[,,\s]+/).filter((v) => v.length > 0);
117
+ return out.length > 0 ? out : fallback;
118
+ }
119
+ return fallback;
120
+ }
121
+
122
+ export function normalizeConfig(source, defaults = DEFAULT_CONFIG) {
123
+ const raw = source !== null && typeof source === 'object' && !Array.isArray(source) ? source : {};
124
+ const merged = { ...defaults, ...raw };
125
+ const families = {};
126
+ const rawFamilies = raw.families !== null && typeof raw.families === 'object' && !Array.isArray(raw.families)
127
+ ? raw.families
128
+ : defaults.families;
129
+ for (const [id, fam] of Object.entries(defaults.families)) {
130
+ const candidate = rawFamilies[id] !== null && typeof rawFamilies[id] === 'object' && !Array.isArray(rawFamilies[id])
131
+ ? rawFamilies[id]
132
+ : fam;
133
+ families[id] = {
134
+ enabled: boolValue(candidate.enabled, fam.enabled),
135
+ tools: stringList(candidate.tools, fam.tools),
136
+ keywords: stringList(candidate.keywords, fam.keywords),
137
+ };
138
+ }
139
+ return {
140
+ enabled: boolValue(merged.enabled, defaults.enabled),
141
+ presets: stringList(merged.presets, defaults.presets),
142
+ suppressRuntimeContext: boolValue(merged.suppressRuntimeContext, defaults.suppressRuntimeContext),
143
+ leanByDefault: boolValue(merged.leanByDefault, defaults.leanByDefault),
144
+ escalateOnKeyword: boolValue(merged.escalateOnKeyword, defaults.escalateOnKeyword),
145
+ escalateOnUnknownTool: boolValue(merged.escalateOnUnknownTool, defaults.escalateOnUnknownTool),
146
+ coreTools: stringList(merged.coreTools, defaults.coreTools),
147
+ families,
148
+ };
149
+ }
150
+
151
+ /** remote.set 的严格校验:非法值直接拒绝并返回错误,而不是静默写坏文件。 */
152
+ export function validateConfig(partial) {
153
+ if (partial === null || typeof partial !== 'object' || Array.isArray(partial)) {
154
+ throw new TypeError('adaptive-perf config must be a plain object');
155
+ }
156
+ const boolFields = ['enabled', 'suppressRuntimeContext', 'leanByDefault', 'escalateOnKeyword', 'escalateOnUnknownTool'];
157
+ for (const key of boolFields) {
158
+ if (partial[key] !== void 0 && typeof partial[key] !== 'boolean') {
159
+ throw new TypeError(`adaptive-perf config field "${key}" must be a boolean`);
160
+ }
161
+ }
162
+ for (const key of ['presets', 'coreTools']) {
163
+ if (partial[key] !== void 0 && !(Array.isArray(partial[key]) && partial[key].every((v) => typeof v === 'string'))) {
164
+ throw new TypeError(`adaptive-perf config field "${key}" must be an array of strings`);
165
+ }
166
+ }
167
+ if (partial.families !== void 0) {
168
+ if (partial.families === null || typeof partial.families !== 'object' || Array.isArray(partial.families)) {
169
+ throw new TypeError('adaptive-perf config field "families" must be an object');
170
+ }
171
+ for (const [id, fam] of Object.entries(partial.families)) {
172
+ if (fam === null || typeof fam !== 'object' || Array.isArray(fam)) {
173
+ throw new TypeError(`adaptive-perf config family "${id}" must be an object { enabled, tools, keywords }`);
174
+ }
175
+ if (fam.enabled !== void 0 && typeof fam.enabled !== 'boolean') {
176
+ throw new TypeError(`adaptive-perf config family "${id}".enabled must be a boolean`);
177
+ }
178
+ for (const key of ['tools', 'keywords']) {
179
+ if (fam[key] !== void 0 && !(Array.isArray(fam[key]) && fam[key].every((v) => typeof v === 'string'))) {
180
+ throw new TypeError(`adaptive-perf config family "${id}".${key} must be an array of strings`);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+
187
+ // ── 纯函数(导出便于回归测试)────────────────────────────────────────────
188
+
189
+ /** 提取一条用户消息的顶层文本(text 块拼接)。 */
190
+ export function extractUserText(message) {
191
+ if (message === null || typeof message !== 'object' || !Array.isArray(message.content)) return '';
192
+ let text = '';
193
+ for (const block of message.content) {
194
+ if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
195
+ text += block.text;
196
+ }
197
+ }
198
+ return text;
199
+ }
200
+
201
+ /** 大小写不敏感的子串匹配:任一关键词命中即返回 true。 */
202
+ export function matchKeywords(text, keywords) {
203
+ if (!Array.isArray(keywords) || keywords.length === 0) return false;
204
+ const haystack = String(text ?? '').toLowerCase();
205
+ return keywords.some((k) => typeof k === 'string' && k.length > 0 && haystack.includes(k.toLowerCase()));
206
+ }
207
+
208
+ /** 收集一次工具调用失败的模型可见文本(error 字段 + content 文本块)。 */
209
+ export function collectFailureText(result) {
210
+ if (result === null || typeof result !== 'object') return '';
211
+ const parts = [];
212
+ const error = result.error;
213
+ if (error !== null && typeof error === 'object') {
214
+ if (typeof error.code === 'string') parts.push(error.code);
215
+ if (typeof error.message === 'string') parts.push(error.message);
216
+ if (typeof error.text === 'string') parts.push(error.text);
217
+ }
218
+ if (Array.isArray(result.content)) {
219
+ for (const block of result.content) {
220
+ if (block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') {
221
+ parts.push(block.text);
222
+ }
223
+ }
224
+ }
225
+ return parts.join('\n');
226
+ }
227
+
228
+ // ── host 插件 ────────────────────────────────────────────────────────────
229
+
230
+ export function apply(ctx, config) {
231
+ // fail-safe:初始化失败只记录,绝不让本插件拖垮 harness(host 层挂载时
232
+ // entry 异常会导致进程启动失败)。
233
+ try {
234
+ const systemPrompt = ctx.get('systemPrompt');
235
+ const tools = ctx.get('tools');
236
+ if (systemPrompt === void 0 || tools === void 0) {
237
+ ctx.logger?.warn?.('adaptive-perf: systemPrompt/tools service unavailable; plugin inactive');
238
+ return;
239
+ }
240
+ const agentPresets = ctx.get('agentPresets');
241
+ if (agentPresets === void 0) {
242
+ ctx.logger?.warn?.('adaptive-perf: agentPresets service unavailable (no agent presets composed); plugin inactive');
243
+ return;
244
+ }
245
+ const agentsService = ctx.get('agents');
246
+
247
+ const patchConfig = config !== null && typeof config === 'object' && !Array.isArray(config) ? config : {};
248
+ /** 当前生效配置(热更新;后续 store 创建后以 config.json 权威合并结果覆盖)。 */
249
+ let cfg = normalizeConfig(patchConfig);
250
+
251
+ /** agentId -> 会话自适应状态。 */
252
+ const agents = new Map();
253
+
254
+ const warn = (message) => { try { ctx.logger?.warn?.(`adaptive-perf: ${message}`); } catch {} };
255
+ const info = (message) => { try { ctx.logger?.info?.(`adaptive-perf: ${message}`); } catch {} };
256
+
257
+ /** 该会话是否为目标 preset。 */
258
+ function isTarget(agent) {
259
+ if (!cfg.enabled) return false;
260
+ try {
261
+ const presetId = agentPresets.composedPreset(agent.ctx);
262
+ return presetId !== void 0 && cfg.presets.includes(presetId);
263
+ } catch (error) {
264
+ warn(`preset lookup failed for agent ${agent.id}: ${error && error.message || error}`);
265
+ return false;
266
+ }
267
+ }
268
+
269
+ /** 计算某族当前应隐藏的工具名(与该 agent 实际可见目录取交集)。 */
270
+ function familyDeny(a, family) {
271
+ if (!Array.isArray(family.tools)) return [];
272
+ return family.tools.filter((n) => typeof n === 'string' && a.visibleNames.has(n));
273
+ }
274
+
275
+ /** 应用/更新运行时上下文抑制。总开关关闭时一并释放。 */
276
+ function applySuppression(a) {
277
+ const want = cfg.enabled && cfg.suppressRuntimeContext;
278
+ if (want && a.suppressDisposer === null) {
279
+ try {
280
+ a.suppressDisposer = a.agent.ctx.systemPrompt.suppressRuntimeContext();
281
+ info(`agent ${a.agent.id}: runtime context suppressed`);
282
+ } catch (error) {
283
+ warn(`suppressRuntimeContext failed for agent ${a.agent.id}: ${error && error.message || error}`);
284
+ }
285
+ } else if (!want && a.suppressDisposer !== null) {
286
+ try { a.suppressDisposer(); } catch {}
287
+ a.suppressDisposer = null;
288
+ }
289
+ }
290
+
291
+ /** 应用/更新工具族限制(lean 模式)。总开关关闭、族被禁用、已升级、
292
+ * 已从配置删除的族一律放行。 */
293
+ function applyFamilies(a) {
294
+ if (!cfg.enabled || !cfg.leanByDefault) {
295
+ for (const [id, disposer] of a.familyDisposers) {
296
+ try { disposer(); } catch {}
297
+ a.familyDisposers.delete(id);
298
+ }
299
+ return;
300
+ }
301
+ // 1) 清理不再需要的限制(族被禁用 / 已升级 / 配置里已不存在)。
302
+ for (const [id, disposer] of a.familyDisposers) {
303
+ const family = cfg.families[id];
304
+ if (family === void 0 || family.enabled === false || a.escalated.has(id)) {
305
+ try { disposer(); } catch {}
306
+ a.familyDisposers.delete(id);
307
+ }
308
+ }
309
+ // 2) 补齐需要的新限制。
310
+ for (const [id, family] of Object.entries(cfg.families)) {
311
+ if (a.familyDisposers.has(id)) continue;
312
+ if (family.enabled === false || a.escalated.has(id)) continue;
313
+ const deny = familyDeny(a, family);
314
+ if (deny.length === 0) continue;
315
+ try {
316
+ const disposer = a.agent.ctx.tools.restrict({ deny });
317
+ a.familyDisposers.set(id, disposer);
318
+ } catch (error) {
319
+ warn(`restrict failed for agent ${a.agent.id} family "${id}": ${error && error.message || error}`);
320
+ }
321
+ }
322
+ }
323
+
324
+ /** 当前隐藏的工具名(活跃限制族的 deny 并集,供失败信号匹配)。 */
325
+ function hiddenNames(a) {
326
+ const out = new Set();
327
+ for (const [id, family] of Object.entries(cfg.families)) {
328
+ if (!a.familyDisposers.has(id)) continue;
329
+ for (const n of familyDeny(a, family)) out.add(n);
330
+ }
331
+ return out;
332
+ }
333
+
334
+ /** 放行一个工具族(会话内单调:只升不降)。 */
335
+ function escalate(a, familyId, trigger) {
336
+ if (a.escalated.has(familyId)) return;
337
+ a.escalated.add(familyId);
338
+ const disposer = a.familyDisposers.get(familyId);
339
+ if (disposer !== void 0) {
340
+ try { disposer(); } catch {}
341
+ a.familyDisposers.delete(familyId);
342
+ }
343
+ info(`agent ${a.agent.id}: escalated family "${familyId}" (${trigger})`);
344
+ }
345
+
346
+ /** 完整接管一个 agent(幂等)。 */
347
+ function handleAgent(agent) {
348
+ if (agents.has(agent.id)) return;
349
+ if (!isTarget(agent)) return;
350
+ const a = {
351
+ agent,
352
+ suppressDisposer: null,
353
+ familyDisposers: new Map(),
354
+ escalated: new Set(),
355
+ visibleNames: new Set(),
356
+ };
357
+ // 记录当前可见目录(限制前);仅记录真实存在的工具,后续 restrict 与
358
+ // 实际目录取交集,preset 升级改名/删除工具不会让本插件崩溃。
359
+ try {
360
+ for (const schema of tools.schemas(agent)) {
361
+ if (schema !== null && typeof schema === 'object' && typeof schema.name === 'string') {
362
+ a.visibleNames.add(schema.name);
363
+ }
364
+ }
365
+ } catch (error) {
366
+ warn(`schemas lookup failed for agent ${agent.id}: ${error && error.message || error}`);
367
+ }
368
+ agents.set(agent.id, a);
369
+ applySuppression(a);
370
+ applyFamilies(a);
371
+ info(`agent ${agent.id}: adaptive-perf active (preset ${String(agentPresets.composedPreset(agent.ctx))})`);
372
+ }
373
+
374
+ /** 释放一个 agent 的全部副作用。 */
375
+ function releaseAgent(agentId) {
376
+ const a = agents.get(agentId);
377
+ if (a === void 0) return;
378
+ agents.delete(agentId);
379
+ if (a.suppressDisposer !== null) {
380
+ try { a.suppressDisposer(); } catch {}
381
+ }
382
+ for (const disposer of a.familyDisposers.values()) {
383
+ try { disposer(); } catch {}
384
+ }
385
+ }
386
+
387
+ // ── 事件监听(注册在 host 根作用域,与 dsh-agent-presets 同款用法)──
388
+ ctx.on('agent/created', ({ agent }) => {
389
+ handleAgent(agent);
390
+ });
391
+ ctx.on('agent/disposed', ({ agent }) => {
392
+ releaseAgent(agent.id);
393
+ });
394
+
395
+ // 关键词信号:用户消息进入收件箱(早于回合处理与请求装配)。
396
+ // 监听器无条件注册,事件时按实时 cfg 判断,热更新开关即时生效。
397
+ ctx.on('agent/inbox/inserted', ({ agent, message }) => {
398
+ if (!cfg.enabled || !cfg.escalateOnKeyword) return;
399
+ const a = agents.get(agent.id);
400
+ if (a === void 0) return;
401
+ const text = extractUserText(message);
402
+ if (text.length === 0) return;
403
+ for (const [id, family] of Object.entries(cfg.families)) {
404
+ if (a.escalated.has(id) || family.enabled === false) continue;
405
+ if (matchKeywords(text, family.keywords)) escalate(a, id, 'keyword');
406
+ }
407
+ });
408
+
409
+ // 失败信号:PTC 程序调用隐藏工具报错(文本含工具名)时放行对应族。
410
+ ctx.on('tools/result', (exec, result) => {
411
+ if (!cfg.enabled || !cfg.escalateOnUnknownTool) return;
412
+ const agent = exec !== null && typeof exec === 'object' ? exec.agent : void 0;
413
+ if (agent === void 0) return;
414
+ const a = agents.get(agent.id);
415
+ if (a === void 0) return;
416
+ if (result === null || typeof result !== 'object' || result.isError !== true) return;
417
+ const text = collectFailureText(result);
418
+ if (text.length === 0) return;
419
+ const hidden = hiddenNames(a);
420
+ if (hidden.size === 0) return;
421
+ for (const [id, family] of Object.entries(cfg.families)) {
422
+ if (a.escalated.has(id) || family.enabled === false) continue;
423
+ for (const n of family.tools) {
424
+ if (hidden.has(n) && text.includes(n)) {
425
+ escalate(a, id, 'unknown-tool');
426
+ break;
427
+ }
428
+ }
429
+ }
430
+ });
431
+
432
+ // 已运行会话补挂(插件热重载/重装时)。
433
+ if (agentsService !== void 0 && typeof agentsService.list === 'function') {
434
+ try {
435
+ for (const agent of agentsService.list()) handleAgent(agent);
436
+ } catch (error) {
437
+ warn(`existing agent sweep failed: ${error && error.message || error}`);
438
+ }
439
+ }
440
+
441
+ // 配置存储:patch config 为低优先级,config.json(设置页 UI)权威。
442
+ // onUpdate 把 UI 保存的改动热更新进 cfg,并对所有已接管会话重算。
443
+ const store = createConfigStore({
444
+ name: 'adaptive-perf',
445
+ defaults: DEFAULT_CONFIG,
446
+ patchConfig,
447
+ validate: validateConfig,
448
+ warn,
449
+ onUpdate: (merged) => {
450
+ cfg = normalizeConfig({ ...patchConfig, ...merged });
451
+ info('config hot-updated; recomputing restrictions for live agents');
452
+ // 补挂此前创建的会话(例如插件从 disabled 切换为 enabled,或新增了
453
+ // 目标 preset):新符合条件的 agent 立即接管。
454
+ if (agentsService !== void 0 && typeof agentsService.list === 'function') {
455
+ try {
456
+ for (const agent of agentsService.list()) handleAgent(agent);
457
+ } catch (error) {
458
+ warn(`config-update agent sweep failed: ${error && error.message || error}`);
459
+ }
460
+ }
461
+ for (const a of agents.values()) {
462
+ applySuppression(a);
463
+ applyFamilies(a);
464
+ }
465
+ },
466
+ });
467
+ cfg = normalizeConfig(store.effective());
468
+
469
+ // 远程配置服务(设置页 UI 读写;typert 不可用时跳过)
470
+ if (PluginConfigGateway !== null) {
471
+ ctx.plugin(PluginConfigGateway, { store, serviceKey: 'adaptivePerfConfig' });
472
+ }
473
+
474
+ // 卸载清理:释放全部会话副作用与监听。
475
+ ctx.effect(() => () => {
476
+ for (const agentId of [...agents.keys()]) releaseAgent(agentId);
477
+ });
478
+ } catch (error) {
479
+ try { ctx.logger?.error?.(`adaptive-perf: init failed (harness continues without adaptive performance): ${error && error.message || error}`); } catch {}
480
+ }
481
+ }
package/lib/remote.mjs ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * remote.mjs — 插件配置的远程服务(设置页 UI 通过 ctx.remote.<svc> 调用)
3
+ *
4
+ * DSH 的 Remote 装饰器是 ECMAScript 标准装饰器语法(Node 默认未启用),
5
+ * 这里用"手动构造装饰器上下文"的方式等价调用:Remote(name)(method, context)
6
+ * 把标记初始化器收集起来,在模块加载时用 Object.create 模拟实例执行
7
+ * (mark 以实例原型为准,与真实实例化等价)。
8
+ *
9
+ * typert-protocol 惰性加载:npm 模式从包内 node_modules 解析;file:// 模式
10
+ * (~/.dsh/plugins/)从 harness 的 profile 依赖树解析。两者都不可用时
11
+ * 模块加载失败,由 index.mjs 的动态 import 捕获——核心功能不受影响,
12
+ * 仅设置页 UI 的配置读写不可用。
13
+ */
14
+
15
+ import { createRequire } from "node:module";
16
+ import { homedir } from "node:os";
17
+ import { join, resolve } from "node:path";
18
+ import { pathToFileURL } from "node:url";
19
+
20
+ // @deepseek-ai/dsh-typert-protocol 是 ESM 包。Node 20 早期版本尚不支持
21
+ // require(ESM),因此统一用 import() 加载;fallback 时先用 createRequire
22
+ // 解析出实际文件,再以 file URL 导入。
23
+ async function loadTypert() {
24
+ try {
25
+ return await import("@deepseek-ai/dsh-typert-protocol");
26
+ } catch {}
27
+ const dshHome = process.env.DSH_HOME?.trim() ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh");
28
+ const candidates = [
29
+ join(dshHome, "profiles", "web", "package.json"),
30
+ join(dshHome, "profiles", "tui", "package.json"),
31
+ join(dshHome, "profiles", "headless", "package.json"),
32
+ ];
33
+ for (const base of candidates) {
34
+ try {
35
+ const resolved = createRequire(base).resolve("@deepseek-ai/dsh-typert-protocol");
36
+ return await import(pathToFileURL(resolved).href);
37
+ } catch {}
38
+ }
39
+ throw new Error("typert-protocol is unavailable (neither package deps nor harness profiles resolve it)");
40
+ }
41
+
42
+ const { Remote, TypertRemoteService } = await loadTypert();
43
+
44
+ let pending = [];
45
+
46
+ /** 手动标记一个类原型方法为 Remote 端点(等价 @Remote(exportName))。 */
47
+ function markRemoteMethod(proto, method, exportName) {
48
+ const initializers = [];
49
+ const context = {
50
+ kind: "method",
51
+ name: method,
52
+ private: false,
53
+ static: false,
54
+ addInitializer(fn) { initializers.push(fn); },
55
+ };
56
+ Remote(exportName ?? method)(proto[method], context);
57
+ pending.push({ initializers });
58
+ }
59
+
60
+ /** 执行收集到的标记(mark 以 Object.getPrototypeOf(this) 为原型)。 */
61
+ function runPendingMarks(instance) {
62
+ const batch = pending;
63
+ pending = [];
64
+ for (const { initializers } of batch) {
65
+ for (const init of initializers) init.call(instance);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 配置远程服务:get() 返回当前生效配置;set(partial) 持久化并热更新。
71
+ * 通过 ctx.plugin(PluginConfigGateway, { store, serviceKey }) 注册。
72
+ * @param ctx - 插件上下文。
73
+ * @param config - { store: createConfigStore 返回的存储, serviceKey: 远程服务名 }。
74
+ */
75
+ export class PluginConfigGateway extends TypertRemoteService {
76
+ constructor(ctx, config) {
77
+ super(ctx, config.serviceKey);
78
+ runPendingMarks(this);
79
+ this.store = config.store;
80
+ }
81
+ get() {
82
+ return { config: this.store.effective() };
83
+ }
84
+ set(partial) {
85
+ if (partial === null || typeof partial !== "object" || Array.isArray(partial)) {
86
+ throw new TypeError("set expects a plain config object");
87
+ }
88
+ this.store.set(partial);
89
+ return { saved: true };
90
+ }
91
+ }
92
+ markRemoteMethod(PluginConfigGateway.prototype, "get");
93
+ markRemoteMethod(PluginConfigGateway.prototype, "set");
94
+ // 模块加载时立即执行标记(Object.create 模拟实例;构造函数里的 runPendingMarks 幂等保留无害)。
95
+ runPendingMarks(Object.create(PluginConfigGateway.prototype));
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@chaoset/adaptive-perf",
3
+ "version": "0.1.0",
4
+ "description": "DSH host plugin: bring standard / PTC presets to minimal-mode-level performance via runtime-context suppression and adaptive tool-catalog trimming with demand-driven escalation",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "files": [
8
+ "lib",
9
+ "client",
10
+ "cordis.patch.yml",
11
+ "README.md"
12
+ ],
13
+ "exports": {
14
+ ".": "./lib/index.mjs",
15
+ "./client": "./client/client.cjs",
16
+ "./cordis.patch.yml": "./cordis.patch.yml",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "dsh": {
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ },
23
+ "client": {
24
+ "platform": "web",
25
+ "inject": [
26
+ "slots",
27
+ "locale",
28
+ "remote",
29
+ "remote.adaptivePerfConfig"
30
+ ]
31
+ }
32
+ },
33
+ "dependencies": {
34
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
35
+ },
36
+ "peerDependencies": {
37
+ "@deepseek-ai/cordis": "^4.0.1"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "scripts": {
43
+ "test": "node ../../scripts/test.mjs",
44
+ "prepublishOnly": "npm test"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/winliyou/dsh-plugins.git",
49
+ "directory": "packages/adaptive-perf"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }