@deepseek-ai/dsh-agent-presets 0.1.5-rc.2 → 0.1.6-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +8 -7
- package/README.zh.md +13 -12
- package/lib/index.js +77 -80
- package/lib/invariant.js +77 -80
- package/lib/typert.host.js +62 -45
- package/lib/typert.remote-client.js +44 -31
- package/lib/types/discovery.d.ts +7 -2
- package/lib/types/discovery.js +31 -47
- package/lib/types/index.d.ts +11 -6
- package/lib/types/index.js +32 -9
- package/lib/types/mount.d.ts +3 -4
- package/lib/types/mount.js +17 -25
- package/lib/types/types.d.ts +2 -0
- package/package.json +28 -27
- package/presets/cordis/agent.cordis.yml +32 -15
- package/presets/cordis/preset.yml +1 -1
- package/presets/cordis/skills/cordis-plugin-development/SKILL.md +73 -385
- package/presets/cordis/skills/editing-cordis-compositions/SKILL.md +11 -74
- package/presets/ptc/agent.cordis.yml +17 -5
- package/presets/standard/agent.cordis.yml +13 -2
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/preset/agent-presets/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: d0eb1c108eaef091c776992c144ba389be2cf6c0
|
|
6
|
+
README.zh.md: e9c1a35b22a1764e444c5757a1e34761bdc24fdf
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ The shipped Web `standard`, `ptc`, and `cordis` presets include [explicit file d
|
|
|
33
33
|
|
|
34
34
|
A session composed from a preset runs the plugins that preset's `agent.cordis.yml` names: its tools, prompt sections, and skills. Sessions joined to the same preset share one installed composition, and each session's state stays separate. A child agent (subagent) joins its parent's composition, so it sees the same tools and prompt sections as the agent that spawned it.
|
|
35
35
|
|
|
36
|
-
The presets you can choose from come from
|
|
36
|
+
The presets you can choose from come from three sources: the presets shipped inside this package under `presets/`, configured roots, and your own presets under `<dshHome>/.agent-presets`. The picker shows each preset's display name and description; a preset whose composition cannot load is listed with the reason rather than hidden, so you can see what to fix or delete.
|
|
37
37
|
|
|
38
38
|
### Minimal configuration
|
|
39
39
|
|
|
@@ -50,7 +50,7 @@ The plugin needs a `default` preset id and scans `roots` for presets:
|
|
|
50
50
|
|
|
51
51
|
| Field | Default | Meaning |
|
|
52
52
|
|---|---|---|
|
|
53
|
-
| `default` | required |
|
|
53
|
+
| `default` | required | Deployment fallback preset id, used while mode selection is disabled or no user default overrides it |
|
|
54
54
|
| `roots` | `[]` | Scanned directories in precedence order; each supplies `path` (a leading `~` expands) and `trust` (defaults to `user`) |
|
|
55
55
|
| `includeShippedRoot` | `true` | Prepend the package's bundled presets as a `system` root before every configured root |
|
|
56
56
|
| `includeUserRoot` | `true` | Append `<dshHome>/.agent-presets` as a `user` root, after every configured root |
|
|
@@ -59,16 +59,17 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
|
|
59
59
|
|
|
60
60
|
The shipped root is prepended before every configured root, so the built-in set remains available and wins duplicate ids even when a patch replaces the roster configuration. `includeShippedRoot: false` drops that built-in set for deployments that supply all presets themselves. `includeUserRoot: false` drops the derived writable root; tests that pin an exact roster disable both derived roots.
|
|
61
61
|
|
|
62
|
-
###
|
|
62
|
+
### Showing the picker and choosing its default
|
|
63
63
|
|
|
64
|
-
The `default` config sets the deployment default. When a settings provider is composed, this plugin registers the `agent-presets` namespace with `config.default` as its base, so a user document
|
|
64
|
+
The required `default` config sets the deployment default. When a settings provider is composed, this plugin registers the `agent-presets` namespace with `{ default: config.default, modeSelectionEnabled: true }` as its base, so the existing new-session picker remains visible unless a user turns it off. The Host reads both fields on every default resolution: while `modeSelectionEnabled` is false, an omitted preset resolves to `config.default` even if the user document retains another `default`; while it is true, a user default may override the deployment value:
|
|
65
65
|
|
|
66
66
|
```yaml
|
|
67
67
|
agent-presets:
|
|
68
|
+
modeSelectionEnabled: true
|
|
68
69
|
default: minimal
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
The
|
|
72
|
+
A client shows or hides selection by writing only `modeSelectionEnabled`; the [Web GUI settings switch](../../client/ui-agent-preset/README.md) does exactly that. The deployment default governs while selection is hidden; re-enabling it restores the saved user `default`, or keeps the deployment default when none has been saved. While mode selection stays enabled, choosing a default writes a user override for sessions created later. Because the Host owns the policy, it applies to every subsequently created session whose caller omits a preset, including Web, CLI, SDK, and headless callers; an explicitly named preset and every existing session remain unchanged.
|
|
72
73
|
|
|
73
74
|
### Authoring presets
|
|
74
75
|
|
|
@@ -82,7 +83,7 @@ A session can switch to a different preset only while it has produced nothing
|
|
|
82
83
|
|
|
83
84
|
### Failures and recovery
|
|
84
85
|
|
|
85
|
-
A preset whose composition is missing, unparsable, not a list of named plugin rows, or naming a module that cannot be resolved is listed as broken with a reason naming the rows at fault;
|
|
86
|
+
A preset whose composition is missing, unparsable, not a list of named plugin rows, or naming a module that cannot be resolved is listed as broken with a reason naming the rows at fault; a package-lookup failure marks only the preset being checked as broken, while the rest of the roster remains available. Composing a broken preset is refused up front, so a session never starts half-composed. What survives to session creation is a row whose module loads and then refuses — a plugin that throws, or one waiting for a service the composition never supplies — which fails the creation and rolls it back, naming every failed row including those inside a group. Fix the preset's file or delete it, then retry.
|
|
86
87
|
|
|
87
88
|
-----
|
|
88
89
|
|
|
@@ -126,7 +127,7 @@ This section explains the design behind the roster and the standing mount; obser
|
|
|
126
127
|
|
|
127
128
|
### The mount audit
|
|
128
129
|
|
|
129
|
-
A directly-plugged subtree is absent from `ctx.loader.entries()`, so no boot audit covers it; `mountPreset` proves the result usable itself and rejects
|
|
130
|
+
A directly-plugged subtree is absent from `ctx.loader.entries()`, so no boot audit covers it; `mountPreset` proves the result usable itself and rejects an import or activation failure, an unscoped target (the preset's tools would register globally), a row still waiting for a service the composition never supplies, and a row that published a service into the root realm (process-global, so the second preset publishing the same name collides). The invariant companion re-checks the last rule on every service notification, because a row publishing from a timer or an asynchronous continuation would escape the one-shot audit.
|
|
130
131
|
|
|
131
132
|
### Authoring mechanics
|
|
132
133
|
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "按 preset cordis.yml 文件进行按会话的 agent
|
|
2
|
+
description: "按 preset cordis.yml 文件进行按会话的 agent(智能体)组装,供选择、配置或排查 agent preset 的用户与维护者阅读。"
|
|
3
3
|
kind: "package-reference"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -31,9 +31,9 @@ kind: "package-reference"
|
|
|
31
31
|
|
|
32
32
|
### preset 给会话带来什么
|
|
33
33
|
|
|
34
|
-
从 preset 组装的会话会运行该 preset `agent.cordis.yml` 所列插件:它的工具、提示词段落与 skill。加入同一 preset
|
|
34
|
+
从 preset 组装的会话会运行该 preset `agent.cordis.yml` 所列插件:它的工具、提示词段落与 skill。加入同一 preset 的会话共享一份已安装的组装,且各会话的状态彼此隔离。subagent 会加入其父方的组装,因此它看到的工具与提示词段落和创建它的 agent 相同。
|
|
35
35
|
|
|
36
|
-
可选的 preset
|
|
36
|
+
可选的 preset 来自三类来源:本包 `presets/` 下随包交付的 preset、已配置的根目录,以及你自己放在 `<dshHome>/.agent-presets` 下的 preset。选择器会展示每个 preset 的显示名与描述;组装无法加载的 preset 会连同原因一起列出而不是被隐藏,因此你能看到该修什么或删什么。
|
|
37
37
|
|
|
38
38
|
### 最小配置
|
|
39
39
|
|
|
@@ -50,7 +50,7 @@ kind: "package-reference"
|
|
|
50
50
|
|
|
51
51
|
| 字段 | 默认值 | 含义 |
|
|
52
52
|
|---|---|---|
|
|
53
|
-
| `default` | 必填 |
|
|
53
|
+
| `default` | 必填 | 部署 fallback preset id;模式选择关闭或没有用户默认值覆盖时使用 |
|
|
54
54
|
| `roots` | `[]` | 按优先级排列的扫描目录;每项提供 `path`(开头的 `~` 会展开)与 `trust`(默认为 `user`) |
|
|
55
55
|
| `includeShippedRoot` | `true` | 在全部已配置根目录之前,前置本包随附的 preset 作为 `system` 根目录 |
|
|
56
56
|
| `includeUserRoot` | `true` | 在全部已配置根目录之后追加 `<dshHome>/.agent-presets` 作为 `user` 根目录 |
|
|
@@ -59,16 +59,17 @@ kind: "package-reference"
|
|
|
59
59
|
|
|
60
60
|
随附根目录前置在全部已配置根目录之前,因此即使补丁替换 roster 配置,内置集合仍然可用并赢得重复 id。`includeShippedRoot: false` 会为完全自行提供 preset 的部署移除内置集合。`includeUserRoot: false` 会移除推导出的可写根目录;钉住确切 roster 的测试会同时关闭两个推导根目录。
|
|
61
61
|
|
|
62
|
-
###
|
|
62
|
+
### 显示选择器并选择默认 preset
|
|
63
63
|
|
|
64
|
-
`default`
|
|
64
|
+
必填的 `default` 配置设定部署默认值。当组装中存在 settings 提供方时,本插件会注册 `agent-presets` 命名空间,并以 `{ default: config.default, modeSelectionEnabled: true }` 作为 base,因此既有的新建会话选择器会保持显示,除非用户主动关闭。Host 每次解析默认值都会读取这两个字段:`modeSelectionEnabled` 为 `false` 时,未显式指定 preset 的会话解析为 `config.default`,即使用户文档还保留其他 `default` 也会忽略它;该字段为 `true` 时,用户默认值才可覆盖部署值:
|
|
65
65
|
|
|
66
66
|
```yaml
|
|
67
67
|
agent-presets:
|
|
68
|
+
modeSelectionEnabled: true
|
|
68
69
|
default: minimal
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
客户端只需写入 `modeSelectionEnabled` 即可显示或隐藏选择,[Web GUI 设置开关](../../client/ui-agent-preset/README.zh.md)正是这样做的。选择器隐藏期间由部署默认值生效;再次开启时恢复已保存的用户 `default`,尚未保存时则继续使用部署默认值。模式选择保持开启时,选择默认模式会写入用户覆盖值,仅供此后创建的会话使用。由于该策略归 Host 所有,它适用于 Web、CLI、SDK 与 headless 调用方此后创建的全部未显式指定 preset 的会话;显式指定的 preset 与任何既有会话均不受影响。
|
|
72
73
|
|
|
73
74
|
### 创作 preset
|
|
74
75
|
|
|
@@ -82,7 +83,7 @@ agent-presets:
|
|
|
82
83
|
|
|
83
84
|
### 失败与恢复
|
|
84
85
|
|
|
85
|
-
组装缺失、无法解析、不是具名插件行列表,或者引用了无法解析的模块的 preset 会被列为 broken
|
|
86
|
+
组装缺失、无法解析、不是具名插件行列表,或者引用了无法解析的模块的 preset 会被列为 broken,原因会指名出问题的行;包查询失败只会把正在检查的 preset 标为 broken,名单中的其他 preset 仍然可用。组装 broken preset 会被提前拒绝,因此会话绝不会以半组装状态启动。能活到会话创建的,是模块能加载但随后拒绝的行——抛错的插件,或等待组装从未提供的服务的插件——它会让创建失败并回滚,且会指名每一个失败的行,包括组内的行。修复 preset 的文件或删除它,然后重试。
|
|
86
87
|
|
|
87
88
|
-----
|
|
88
89
|
|
|
@@ -113,7 +114,7 @@ agent-presets:
|
|
|
113
114
|
| [`src/authoring.ts`](src/authoring.ts) | 本地创作 preset 的复制/删除/读取、权限收紧 |
|
|
114
115
|
| [`src/metadata.ts`](src/metadata.ts) | `preset.yml` 展示元数据 |
|
|
115
116
|
| [`src/session.ts`](src/session.ts) | `agent-preset/selected` 事件与 `agentPreset` Session 投影 |
|
|
116
|
-
| [`src/types.ts`](src/types.ts) | client-safe
|
|
117
|
+
| [`src/types.ts`](src/types.ts) | client-safe 的协议载荷与 cordis 事件声明 |
|
|
117
118
|
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:挂载后的服务泄漏复查、未加入 agent 的失败 |
|
|
118
119
|
|
|
119
120
|
### 常驻挂载
|
|
@@ -122,11 +123,11 @@ agent-presets:
|
|
|
122
123
|
|
|
123
124
|
### 组合清单
|
|
124
125
|
|
|
125
|
-
`compositionInventory()` 向插件清单表面提供每个预设的压平行及其名单身份(id、trust、显示名、默认标记):已有存活 standing mount 的预设由其最新世代的 Loader 条目作答——匹配限定在本运行时自己的 root 内,同进程里的第二个 Cordis 运行时不会替它作答;即使文件事后损坏也照常作答,因为挂载才是会话实际运行的组合,broken 裁决只适用于无人组合的预设——开机以来从未被组合的预设由其组合文件作答,`!!js` disabled 门用 Loader 上下文求值,使两种答案反映同一台宿主。读取从不挂载预设——列出所有组合的设置页不会激活其中任何一个。求值器拒绝的门保持 `'conditional'`;在发现的健康裁决与行读取之间变得不可读的文件,会携带竞态原因报告为 broken,而不是被静默丢弃。`./display` 子路径导出 `presetDisplayText`
|
|
126
|
+
`compositionInventory()` 向插件清单表面提供每个预设的压平行及其名单身份(id、trust、显示名、默认标记):已有存活 standing mount 的预设由其最新世代的 Loader 条目作答——匹配限定在本运行时自己的 root 内,同进程里的第二个 Cordis 运行时不会替它作答;即使文件事后损坏也照常作答,因为挂载才是会话实际运行的组合,broken 裁决只适用于无人组合的预设——开机以来从未被组合的预设由其组合文件作答,`!!js` disabled 门用 Loader 上下文求值,使两种答案反映同一台宿主。读取从不挂载预设——列出所有组合的设置页不会激活其中任何一个。求值器拒绝的门保持 `'conditional'`;在发现的健康裁决与行读取之间变得不可读的文件,会携带竞态原因报告为 broken,而不是被静默丢弃。`./display` 子路径导出 `presetDisplayText` 映射,把随附 preset id 映射到各自的字典文案键;它没有任何 import,浏览器包直接内联,也是「哪个内置 id 对应哪份文案」的唯一归属地。
|
|
126
127
|
|
|
127
128
|
### 挂载审计
|
|
128
129
|
|
|
129
|
-
直接挂载的子树不会出现在 `ctx.loader.entries()` 中,因此没有启动审计能覆盖它;`mountPreset`
|
|
130
|
+
直接挂载的子树不会出现在 `ctx.loader.entries()` 中,因此没有启动审计能覆盖它;`mountPreset` 自行证明结果可用,并拒绝导入或激活失败、无 scope 的目标(preset 的工具会注册成全局的)、仍在等待组装从未提供的服务的行、以及把服务发布进根 realm 的行(进程级全局,第二个发布同名服务的 preset 会相撞)。不变式伴生插件在每次服务通知时复查最后一条规则,因为从定时器或异步续体发布的行会绕过一次性审计。
|
|
130
131
|
|
|
131
132
|
### 创作机制
|
|
132
133
|
|
|
@@ -175,7 +176,7 @@ agent-presets:
|
|
|
175
176
|
- **代际只以组装文件为键**——stamp 检查只察觉 `agent.cordis.yml` 的变化,察觉不到旁边 skill 文件或资产的编辑;那些编辑要等组装文件本身变动或进程重启才达到新会话。
|
|
176
177
|
- **被替代的代际永不回收**——已加入的会话保持其运行所在的代际,而名单没有加入计数可以判断最后一个何时离开,因此整棵子树一直挂到进程结束。代价按代际计而非按会话计,但并非为零:`dsh-skill-filesystem` 默认监听自己的根目录,因此每一轮「编辑后建会话」都会新增一套活的 watcher。
|
|
177
178
|
- **副本从不被实际挂载以校验**——它与来源逐字节相同,因此磁盘上已坏的来源会产出与来源同样损坏的副本;发现过程的健康检查会在下一次读取名单时把两行都标出来,而不是把失败推迟到会话启动。
|
|
178
|
-
- **健康问的是「装没装」,不是「能不能 import
|
|
179
|
+
- **健康问的是「装没装」,不是「能不能 import」**——发现过程证明组装能以加载器方言解析、由具名行组成,且对于每个能证明会启动的行,其引用的包存在于 harness 基址以上,或引用的文件确实存在;它从不 import 任何一个,因此入口文件缺失的包、在 apply 时抛错的插件、以及永远等待某个服务的插件,都仍在第一个会话处失败。`disabled` 是加载器唯一会插值的条目字段,因此该字段带表达式的行不予检查,而不是仅凭文件作出判断。
|
|
179
180
|
- **副本是会漂移的快照**——升级部署不会更新随附 preset 的副本,本层也没有表达「standard 加一处改动」的 patch 语义;随附集合自己也接受同样的代价——`cordis` 与 `code` 都复制了 `standard` 的完整组装并在此基础上编辑——换来整份组装在一个文件里可读。
|
|
180
181
|
- **根目录扫描不做监听**——每次读取都实际访问文件系统,这让名单保持新鲜,但每次 `list()` 会对每个根目录产生一次 `readdir`。
|
|
181
182
|
|
package/lib/index.js
CHANGED
|
@@ -230,20 +230,13 @@ function entryListProblem(rows, at = "") {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
/**
|
|
233
|
-
* Whether a package
|
|
233
|
+
* Whether a package specifier is installed above `base` without importing it.
|
|
234
234
|
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* `existsSync` rather than the async `stat`: the walk is a handful of lookups
|
|
242
|
-
* per package and runs on every roster read, where 150 promise round-trips
|
|
243
|
-
* cost more than the lookups they wrap.
|
|
244
|
-
* @param name - the package specifier, possibly carrying a subpath.
|
|
245
|
-
* @param base - the URL to walk up from.
|
|
246
|
-
* @returns true when the package directory is installed above `base`.
|
|
235
|
+
* The direct disk walk accepts unexported subpaths and rejects stale links
|
|
236
|
+
* whose package directory no longer exists.
|
|
237
|
+
* @param name - package specifier, possibly carrying a subpath.
|
|
238
|
+
* @param base - directory URL whose ancestors contain candidate `node_modules` directories.
|
|
239
|
+
* @returns true when the package is installed.
|
|
247
240
|
*/
|
|
248
241
|
function packageInstalled(name, base) {
|
|
249
242
|
const pkg = name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/");
|
|
@@ -258,35 +251,17 @@ function packageInstalled(name, base) {
|
|
|
258
251
|
/**
|
|
259
252
|
* Whether one classified row names a module that exists, importing nothing.
|
|
260
253
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* relative or `file:` specifier is statted, because both name one file.
|
|
264
|
-
* Nothing is evaluated either way, so a row is judged without its plugin
|
|
265
|
-
* observing that discovery looked.
|
|
266
|
-
*
|
|
267
|
-
* `import.meta.resolve` is deliberately not the fallback for a name the disk
|
|
268
|
-
* lookup misses. Its `parentURL` argument only takes effect under
|
|
269
|
-
* `--experimental-import-meta-resolve`, which no launch passes, so it would
|
|
270
|
-
* resolve from THIS module rather than from the harness — reporting a
|
|
271
|
-
* dependency visible only to this package as healthy, and a plugin the mount
|
|
272
|
-
* can import as broken. The resolver that does honour an explicit parent is
|
|
273
|
-
* the Loader's internal one, whose `resolveSync` signature differs between
|
|
274
|
-
* Node 22 and 24 (`ModuleLoader.fromInternal` tags the raw object rather than
|
|
275
|
-
* normalising it); reaching into that for a case the walk already covers buys
|
|
276
|
-
* nothing a supported deployment needs, because every plugin a preset names
|
|
277
|
-
* is installed beside the roster.
|
|
278
|
-
*
|
|
279
|
-
* What that gives up: a package resolvable ONLY through a loader hook — an
|
|
280
|
-
* import map, or a tree with no `node_modules` at all — is reported broken.
|
|
281
|
-
* No supported install produces one.
|
|
254
|
+
* Package rows delegate to the injected lookup. Relative and `file:` rows use
|
|
255
|
+
* file metadata. No check evaluates the named module.
|
|
282
256
|
* @param row - the classified specifier, from {@link classifyRowSpecifier}.
|
|
283
257
|
* @param presetBase - directory URL a preset-relative specifier resolves against.
|
|
284
258
|
* @param harnessBase - base URL a package name resolves against.
|
|
259
|
+
* @param resolves - package lookup selected by the owning caller.
|
|
285
260
|
* @returns true when the row names something that can be imported.
|
|
286
261
|
*/
|
|
287
|
-
async function rowResolves(row, presetBase, harnessBase) {
|
|
262
|
+
async function rowResolves(row, presetBase, harnessBase, resolves) {
|
|
288
263
|
if (row.kind === "builtin") return true;
|
|
289
|
-
if (row.kind === "package") return isBuiltin(row.specifier) ||
|
|
264
|
+
if (row.kind === "package") return isBuiltin(row.specifier) || resolves(row.specifier, harnessBase);
|
|
290
265
|
return await isFile(fileURLToPath(row.kind === "file" ? new URL(row.specifier) : new URL(row.specifier, presetBase)));
|
|
291
266
|
}
|
|
292
267
|
/**
|
|
@@ -310,17 +285,17 @@ async function rowResolves(row, presetBase, harnessBase) {
|
|
|
310
285
|
* @param at - row-path prefix for nested diagnostics, empty at the top level.
|
|
311
286
|
* @returns one entry per unresolvable row, in composition order.
|
|
312
287
|
*/
|
|
313
|
-
async function unresolvableRows(rows, presetBase, harnessBase, at = "") {
|
|
288
|
+
async function unresolvableRows(rows, presetBase, harnessBase, resolves, at = "") {
|
|
314
289
|
const found = [];
|
|
315
290
|
for (const [index, entry] of rows.entries()) {
|
|
316
291
|
const row = entry;
|
|
317
292
|
if (Boolean(row.disabled)) continue;
|
|
318
293
|
const positional = at === "" ? `row ${String(index + 1)}` : `${at} row ${String(index + 1)}`;
|
|
319
294
|
if (row.group === true) {
|
|
320
|
-
found.push(...await unresolvableRows(row.config, presetBase, harnessBase, positional));
|
|
295
|
+
found.push(...await unresolvableRows(row.config, presetBase, harnessBase, resolves, positional));
|
|
321
296
|
continue;
|
|
322
297
|
}
|
|
323
|
-
if (await rowResolves(classifyRowSpecifier(row.name), presetBase, harnessBase)) continue;
|
|
298
|
+
if (await rowResolves(classifyRowSpecifier(row.name), presetBase, harnessBase, resolves)) continue;
|
|
324
299
|
const label = typeof row.id === "string" && row.id !== "" ? `row "${row.id}"` : positional;
|
|
325
300
|
found.push({
|
|
326
301
|
label,
|
|
@@ -334,11 +309,13 @@ async function unresolvableRows(rows, presetBase, harnessBase, at = "") {
|
|
|
334
309
|
* loadable. Parsed with the loader's own YAML dialect ({@link entryListSchema},
|
|
335
310
|
* the one carrying `!!js`), so health can never call a composition broken
|
|
336
311
|
* that the loader would accept.
|
|
312
|
+
* A package-lookup failure becomes this composition's broken reason, so one
|
|
313
|
+
* preset cannot abort discovery of the rest of the roster.
|
|
337
314
|
* @param path - absolute path of the composition file.
|
|
338
315
|
* @param harnessBase - base URL a row's package name resolves against.
|
|
339
316
|
* @returns one human-readable reason, or undefined when the file is loadable.
|
|
340
317
|
*/
|
|
341
|
-
async function compositionProblem(path, harnessBase) {
|
|
318
|
+
async function compositionProblem(path, harnessBase, resolves) {
|
|
342
319
|
let content;
|
|
343
320
|
try {
|
|
344
321
|
content = await readFile(path, "utf8");
|
|
@@ -354,7 +331,12 @@ async function compositionProblem(path, harnessBase) {
|
|
|
354
331
|
const shape = entryListProblem(rows);
|
|
355
332
|
if (shape !== void 0) return shape;
|
|
356
333
|
const presetBase = new URL(".", pathToFileURL(path)).href;
|
|
357
|
-
|
|
334
|
+
let unresolvable;
|
|
335
|
+
try {
|
|
336
|
+
unresolvable = await unresolvableRows(rows, presetBase, harnessBase, resolves);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return `the composition's plugins cannot be checked: ${(error instanceof Error ? error.message : String(error)).replace(/\n[\s\S]*$/, "")}`;
|
|
339
|
+
}
|
|
358
340
|
const [first] = unresolvable;
|
|
359
341
|
if (first === void 0) return void 0;
|
|
360
342
|
if (unresolvable.length === 1) return `${first.label} names a plugin that cannot be resolved: ${first.name}`;
|
|
@@ -387,9 +369,10 @@ async function isFile(path) {
|
|
|
387
369
|
* @param root - the directory and the trust its presets inherit.
|
|
388
370
|
* @param harnessBase - base URL a row's package name resolves against; the
|
|
389
371
|
* caller's own `ctx.baseUrl`, which is where the installed harness lives.
|
|
372
|
+
* @param resolves - package-presence lookup for the active runtime.
|
|
390
373
|
* @returns the root's presets ordered by id.
|
|
391
374
|
*/
|
|
392
|
-
async function scanRoot(root, harnessBase) {
|
|
375
|
+
async function scanRoot(root, harnessBase, resolves = packageInstalled) {
|
|
393
376
|
const dir = resolve(expandHomePath(root.path));
|
|
394
377
|
let children;
|
|
395
378
|
try {
|
|
@@ -403,7 +386,7 @@ async function scanRoot(root, harnessBase) {
|
|
|
403
386
|
if (!child.isDirectory() || !PRESET_ID.test(child.name)) continue;
|
|
404
387
|
const directory = join(dir, child.name);
|
|
405
388
|
const path = join(directory, COMPOSITION_FILE);
|
|
406
|
-
const broken = await isFile(path) ? await compositionProblem(path, harnessBase) : `the composition file ${COMPOSITION_FILE} is missing — the directory still occupies the id; delete it or restore the file`;
|
|
389
|
+
const broken = await isFile(path) ? await compositionProblem(path, harnessBase, resolves) : `the composition file ${COMPOSITION_FILE} is missing — the directory still occupies the id; delete it or restore the file`;
|
|
407
390
|
const metadata = await readPresetMetadata(directory);
|
|
408
391
|
found.push({
|
|
409
392
|
id: child.name,
|
|
@@ -422,11 +405,12 @@ async function scanRoot(root, harnessBase) {
|
|
|
422
405
|
* Scan every root in precedence order.
|
|
423
406
|
* @param roots - roots in precedence order; an earlier root wins a duplicate id.
|
|
424
407
|
* @param harnessBase - base URL a row's package name resolves against.
|
|
408
|
+
* @param resolves - package-presence lookup for the active runtime.
|
|
425
409
|
* @returns every discovered preset, first-root-wins per id.
|
|
426
410
|
*/
|
|
427
|
-
async function discoverPresets(roots, harnessBase) {
|
|
411
|
+
async function discoverPresets(roots, harnessBase, resolves = packageInstalled) {
|
|
428
412
|
const byId = /* @__PURE__ */ new Map();
|
|
429
|
-
for (const root of roots) for (const preset of await scanRoot(root, harnessBase)) {
|
|
413
|
+
for (const root of roots) for (const preset of await scanRoot(root, harnessBase, resolves)) {
|
|
430
414
|
if (byId.has(preset.id)) continue;
|
|
431
415
|
byId.set(preset.id, preset);
|
|
432
416
|
}
|
|
@@ -830,24 +814,28 @@ function serviceForAgent(ctx, agent, name) {
|
|
|
830
814
|
/**
|
|
831
815
|
* Rows that did not reach a usable state, each rendered as one diagnostic line.
|
|
832
816
|
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
* for a service the composition never supplies.
|
|
817
|
+
* Wait for the subtree, then report import failures, activation failures, and
|
|
818
|
+
* rows waiting for services the composition does not supply.
|
|
836
819
|
* @param tree - the mounted subtree.
|
|
837
820
|
* @returns one line per unusable row, empty when every enabled row is usable.
|
|
838
821
|
*/
|
|
839
|
-
function inactiveRows(tree) {
|
|
822
|
+
async function inactiveRows(tree) {
|
|
823
|
+
await tree.await();
|
|
840
824
|
const lines = [];
|
|
841
825
|
for (const entry of tree.entries()) {
|
|
842
826
|
if (entry.disabled) continue;
|
|
843
827
|
const fiber = entry.fiber;
|
|
844
|
-
/* v8 ignore next 4 -- the loader rejects an entry whose module or plugin failed,
|
|
845
|
-
so a settled tree never holds an enabled fiber-less entry; the branch exists
|
|
846
|
-
only because `Entry.fiber` is declared optional. */
|
|
847
828
|
if (fiber === void 0) {
|
|
848
829
|
lines.push(`${entry.options.id} (${entry.options.name}): never started`);
|
|
849
830
|
continue;
|
|
850
831
|
}
|
|
832
|
+
try {
|
|
833
|
+
await fiber.await();
|
|
834
|
+
} catch (error) {
|
|
835
|
+
const detail = mountDetail(error);
|
|
836
|
+
lines.push(`${entry.options.id} (${entry.options.name}): ${detail}`);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
851
839
|
const missing = Object.keys(fiber.inject).filter((name) => fiber.ctx.get(name) === void 0);
|
|
852
840
|
if (missing.length > 0) lines.push(`${entry.options.id} (${entry.options.name}): waiting for ${missing.join(", ")}`);
|
|
853
841
|
}
|
|
@@ -856,14 +844,8 @@ function inactiveRows(tree) {
|
|
|
856
844
|
/**
|
|
857
845
|
* The causes of `error` whose detail its own message does not already carry.
|
|
858
846
|
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
* approach: it appends `cause.message` to the message it builds and keeps the
|
|
862
|
-
* cause only as `error.cause`, so following a plain chain would print every
|
|
863
|
-
* line twice. That leaves exactly one lossy shape — a wrapped row whose cause
|
|
864
|
-
* is an `AggregateError`. Its message ends with the aggregate's own line and
|
|
865
|
-
* drops the `errors` behind it, which is how a failed group reports as
|
|
866
|
-
* "loader entries failed to apply" and names none of the rows that failed.
|
|
847
|
+
* Aggregate errors carry separate member messages. A wrapper can preserve the
|
|
848
|
+
* aggregate as its cause without including those messages in its own text.
|
|
867
849
|
* @param error - the failure to read branches from.
|
|
868
850
|
* @returns the branches to render beneath `error.message`, possibly empty.
|
|
869
851
|
*/
|
|
@@ -874,19 +856,12 @@ function detailBranches(error) {
|
|
|
874
856
|
/**
|
|
875
857
|
* The reportable text of a mount failure.
|
|
876
858
|
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
* two rows says only "loader entries failed to apply" and the operator has
|
|
880
|
-
* nothing to act on. Nested groups indent under the row that owns them, so a
|
|
881
|
-
* composition failing inside a group still names the rows rather than the
|
|
882
|
-
* group alone.
|
|
859
|
+
* A plugin may reject with an aggregate or wrap one as its cause. Include its
|
|
860
|
+
* member messages beneath the row diagnostic so each failure is visible.
|
|
883
861
|
* @param error - the value the mount rejected with.
|
|
884
862
|
* @returns a single-line-per-cause description.
|
|
885
863
|
*/
|
|
886
864
|
function mountDetail(error) {
|
|
887
|
-
/* v8 ignore next -- every path into the mount's catch throws an Error: the loader
|
|
888
|
-
wraps a row's thrown value before it propagates, and this module's own
|
|
889
|
-
rejections are Errors. The fallback keeps a hostile value readable. */
|
|
890
865
|
if (!(error instanceof Error)) return String(error);
|
|
891
866
|
const branches = detailBranches(error);
|
|
892
867
|
if (branches.length === 0) return error.message;
|
|
@@ -915,7 +890,7 @@ async function mountPreset(agentCtx, preset) {
|
|
|
915
890
|
/* v8 ignore next -- the subclass constructor runs before `await()` settles for every mounted tree */
|
|
916
891
|
if (subtree === void 0) throw new Error("mounted subtree did not publish its entry tree");
|
|
917
892
|
const { tree, fiber } = subtree;
|
|
918
|
-
const unusable = inactiveRows(tree);
|
|
893
|
+
const unusable = await inactiveRows(tree);
|
|
919
894
|
if (unusable.length > 0) throw new Error(`${String(unusable.length)} row(s) did not activate:\n${unusable.join("\n")}`);
|
|
920
895
|
const leaked = leakedServices(agentCtx, fiber);
|
|
921
896
|
if (leaked.length > 0) throw new Error(`row(s) published process-global service(s) [${leaked.join(", ")}]; a preset service must sit behind an \`isolate\` realm or move to the host composition`);
|
|
@@ -1141,14 +1116,17 @@ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializ
|
|
|
1141
1116
|
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
1142
1117
|
done = true;
|
|
1143
1118
|
};
|
|
1144
|
-
/** Settings namespace carrying the user's chosen default
|
|
1119
|
+
/** Settings namespace carrying the user's preset-picker preference and chosen default. */
|
|
1145
1120
|
const SETTINGS_NAMESPACE = "agent-presets";
|
|
1146
1121
|
/** Refuse an empty preset id before invoking a domain operation. */
|
|
1147
1122
|
function validatePresetId(value, field) {
|
|
1148
1123
|
if (value.length === 0) throw new RemoteError("gateway/bad-request", `${field} must be a non-empty string`, {});
|
|
1149
1124
|
}
|
|
1150
1125
|
/** Runtime schema for the user-writable slice. */
|
|
1151
|
-
const AgentPresetSettingsSchema = z.object({
|
|
1126
|
+
const AgentPresetSettingsSchema = z.object({
|
|
1127
|
+
default: z.string(),
|
|
1128
|
+
modeSelectionEnabled: z.boolean()
|
|
1129
|
+
});
|
|
1152
1130
|
/**
|
|
1153
1131
|
* Registry over the deployment's agent presets.
|
|
1154
1132
|
*
|
|
@@ -1309,7 +1287,10 @@ let AgentPresets = (() => {
|
|
|
1309
1287
|
}] : []
|
|
1310
1288
|
];
|
|
1311
1289
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
1312
|
-
this.settings = settingsCtx.settings.register(SETTINGS_NAMESPACE, AgentPresetSettingsSchema, { base: {
|
|
1290
|
+
this.settings = settingsCtx.settings.register(SETTINGS_NAMESPACE, AgentPresetSettingsSchema, { base: {
|
|
1291
|
+
default: config.default,
|
|
1292
|
+
modeSelectionEnabled: true
|
|
1293
|
+
} });
|
|
1313
1294
|
this.settingsService = settingsCtx.settings;
|
|
1314
1295
|
settingsCtx.effect(() => () => {
|
|
1315
1296
|
this.settings = void 0;
|
|
@@ -1335,35 +1316,51 @@ let AgentPresets = (() => {
|
|
|
1335
1316
|
* every running session on the preset it was composed from.
|
|
1336
1317
|
*/
|
|
1337
1318
|
get defaultId() {
|
|
1338
|
-
return this.
|
|
1319
|
+
return this.selectionPolicy().defaultId;
|
|
1320
|
+
}
|
|
1321
|
+
/** Read one internally consistent snapshot of the selection policy. */
|
|
1322
|
+
selectionPolicy() {
|
|
1323
|
+
const settings = this.settings?.get();
|
|
1324
|
+
if (settings === void 0) return {
|
|
1325
|
+
enabled: true,
|
|
1326
|
+
defaultId: this.config.default
|
|
1327
|
+
};
|
|
1328
|
+
const enabled = settings.modeSelectionEnabled;
|
|
1329
|
+
return {
|
|
1330
|
+
enabled,
|
|
1331
|
+
defaultId: enabled ? settings.default : this.config.default
|
|
1332
|
+
};
|
|
1339
1333
|
}
|
|
1340
1334
|
/**
|
|
1341
1335
|
* Every preset the configured roots currently supply.
|
|
1342
1336
|
* @returns the presets, first-root-wins per id.
|
|
1343
1337
|
*/
|
|
1344
1338
|
async list() {
|
|
1345
|
-
|
|
1339
|
+
const packages = this.ctx.get("pluginPackages");
|
|
1340
|
+
return packages === void 0 ? await discoverPresets(this.resolvedRoots, this.harnessBase) : await discoverPresets(this.resolvedRoots, this.harnessBase, (specifier, base) => packages.packageOf(specifier, base) !== void 0);
|
|
1346
1341
|
}
|
|
1347
1342
|
/**
|
|
1348
1343
|
* The roster off the Host: {@link list} projected to path-free rows, with
|
|
1349
|
-
* the default marked
|
|
1344
|
+
* the policy-effective default marked, this deployment's authoring
|
|
1345
|
+
* capability, and its mode-selection policy beside it.
|
|
1350
1346
|
*
|
|
1351
1347
|
* Whether a client can open a preset's directory is the Host's own opener
|
|
1352
1348
|
* capability, not a roster property — a caller needing both joins them.
|
|
1353
|
-
* @returns the rows and
|
|
1349
|
+
* @returns the rows, authoring capability, and effective selection policy.
|
|
1354
1350
|
*/
|
|
1355
1351
|
async remoteExportList() {
|
|
1356
|
-
const
|
|
1352
|
+
const policy = this.selectionPolicy();
|
|
1357
1353
|
return {
|
|
1358
1354
|
presets: (await this.list()).map((preset) => ({
|
|
1359
1355
|
id: preset.id,
|
|
1360
1356
|
trust: preset.trust,
|
|
1361
|
-
isDefault: preset.id === defaultId,
|
|
1357
|
+
isDefault: preset.id === policy.defaultId,
|
|
1362
1358
|
...preset.name === void 0 ? {} : { name: preset.name },
|
|
1363
1359
|
...preset.description === void 0 ? {} : { description: preset.description },
|
|
1364
1360
|
...preset.broken === void 0 ? {} : { broken: preset.broken }
|
|
1365
1361
|
})),
|
|
1366
|
-
authorable: this.authorable
|
|
1362
|
+
authorable: this.authorable,
|
|
1363
|
+
modeSelectionEnabled: policy.enabled
|
|
1367
1364
|
};
|
|
1368
1365
|
}
|
|
1369
1366
|
/**
|