dsh-agent-foreman 0.0.0-stage → 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 Dclef
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 CHANGED
@@ -1,3 +1,151 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # Foreman · dsh-agent-foreman
2
+
3
+ > Parent agent dispatches, sub-agents do the work, the parent reviews.
4
+ > No preset roles, no scripted phases: **you decide who works and with which model**.
5
+
6
+ Chinese docs: [README.zh.md](README.zh.md)
7
+
8
+ ## The problem
9
+
10
+ DSH's built-in Agent Team lets you give each teammate a name and a prompt — but **not a model**.
11
+ Real work is mixed: some of it needs an expensive model, some of it is a two-line edit.
12
+ Inheriting the parent's model for every pair of hands is both slow and expensive.
13
+
14
+ Foreman splits it in two:
15
+
16
+ - **Contractors** — the crew you define in the settings page, named by trade like a real
17
+ construction site: 力工 (laborer — does the business work) and, optionally, 安全员 (safety officer
18
+ — reviews and rejects). The foreman itself is the parent agent: it splits the work and signs off,
19
+ and is never in the roster. Want specialties? **Hire several instances** (力工 · frontend,
20
+ 力工 · render), each with its own model and **its own prompt** (the 👁 button next to the name),
21
+ and its own concurrency cap: default 1, so one instance never works two orders at once; set it to
22
+ 2 and it can. That prompt is prepended to every order the contractor receives. **The minimal setup
23
+ is a single 力工.**
24
+ - **Orders** — the work the parent agent splits out. Each order carries a task and acceptance
25
+ criteria, and may name a contractor or rotate automatically.
26
+
27
+ The parent only splits work and reviews results. Everything in between happens in isolated child
28
+ sessions, and the products come back as one shared board.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ dsh plugin add E:/path/to/dsh-agent-foreman
34
+ ```
35
+
36
+ Manual (dev) install:
37
+
38
+ 1. Copy the whole directory to `<profile>/node_modules/dsh-agent-foreman`
39
+ 2. Add `"dsh-agent-foreman"` to `dsh.profile.bundles` in `<profile>/package.json`
40
+ 3. Restart DSH
41
+
42
+ Then open **Settings → 包工头** and add contractors.
43
+
44
+ ## Usage
45
+
46
+ ![Settings → 包工头: the contractor roster, with a per-contractor prompt behind the eye button](docs/settings-roster.png)
47
+
48
+ Type `/foreman` (registered at runtime through `ctx.skills.register()`) followed by the work:
49
+ `/foreman extract GraphBlocked into shared logic; add regression tests`. Semicolons or newlines
50
+ split orders. The skill body walks the parent through roster → split → one dispatch → review, so no
51
+ standing system prompt is needed.
52
+
53
+ | Tool | Purpose |
54
+ | --- | --- |
55
+ | `foreman_roster` | List contractors with route, prompt length, concurrency cap, and the current policy |
56
+ | `foreman_dispatch` | Dispatch a batch of orders, wait for all of them, return the board |
57
+ | `foreman_review` | Pass / fail each order; a failed one is re-dispatched with feedback |
58
+ | `foreman_status` | Re-read a board; omit `runId` for the latest one in this session |
59
+
60
+ ### Where members show up
61
+
62
+ There is no custom in-chat panel any more: in team mode a contractor **is** a real Agent
63
+ Teams member, so the native `Agent Teams` button in the session header lists them and
64
+ shows each one's model. The settings page keeps the full roster editor.
65
+
66
+ Roster notes:
67
+
68
+ - A contractor is a name, an optional prompt, a provider, a model, an effort level and a concurrency cap.
69
+ **Leave the provider on "follow the current session" and the child simply runs on whatever model
70
+ the parent session is using.**
71
+ - To use a different model: pick the provider first — **the model dropdown is derived from it** and
72
+ only lists that provider's models (providers with no models never show up).
73
+ - Settings page collapses policy behind "派单策略"; the roster table keeps name / provider / model /
74
+ effort / concurrency / enabled, and 👁 opens that contractor's prompt editor.
75
+
76
+ Dispatch notes:
77
+
78
+ - `orders[].task` **must be self-contained** — the child cannot see the parent's context.
79
+ - `orders[].contractor` rotates through the roster when omitted; unknown names fail loudly.
80
+ - `orders[].dependsOn` only references orders in the same batch; dependents run serially after.
81
+ - `waitSeconds` caps the whole batch; unfinished orders are aborted rather than silently reported done.
82
+ - `concurrency` defaults to 4, capped at 8.
83
+
84
+ ## Policy
85
+
86
+ | Field | Default | Meaning |
87
+ | --- | --- | --- |
88
+ | `concurrency` | 4 | Contractors working at once (1–8); the per-instance cap lives in the roster |
89
+ | `timeoutSeconds` | 900 | Wall-clock cap per order |
90
+ | `maxRework` | 1 | How many times an order may be sent back |
91
+ | `maxDepth` | 1 | Whether a child may dispatch further children |
92
+ | `provider` | `spawn` | Which subagents provider to use (`spawn` / `fork` / `cursor`) |
93
+
94
+ ## Data
95
+
96
+ - Windows: `%LOCALAPPDATA%/dsh-agent-foreman/`
97
+ - Elsewhere: `<baseDir>/data/dsh-agent-foreman/`
98
+
99
+ `roster.json` (contractors + policy), `runs.jsonl` (one line per dispatch round).
100
+ A corrupt roster never blocks startup — it is treated as empty and logged.
101
+
102
+ ## Security
103
+
104
+ - Every HTTP route accepts same-origin, loopback requests only; cross-site calls get 403.
105
+ - The roster stores provider/model/effort names — never credentials.
106
+ - Contractors run in their own child sessions and inherit DSH's effective sandbox.
107
+
108
+ ## Development
109
+
110
+ ```bash
111
+ node --test test/*.test.mjs # 26 cases: tools, engine, routes, CSRF, ledger
112
+ ```
113
+
114
+ `test/harness.test.mjs` runs the real host half against a fake host, so no DSH instance is needed.
115
+
116
+ ## Compared to
117
+
118
+ | | Agent Team (built-in) | swarm-orchestrator | Foreman |
119
+ | --- | --- | --- | --- |
120
+ | Per-member model | no | no | **yes** |
121
+ | Preset roles | fixed | 4 fixed roles | none — you define |
122
+ | Shared board | yes | yes | yes |
123
+ | Rework loop | no | limited | yes, with feedback |
124
+ | Settings page | yes | no | yes, roster + live model catalog |
125
+
126
+ ## Team mode (experimental)
127
+
128
+ Off by default. Tick **团队模式** under Settings → 包工头 → 派单策略 and each
129
+ contractor becomes a real member of the host's **agent team**: the plugin spawns
130
+ it with `agentTeams.spawnTeammate`, switches its session model with
131
+ `sessions.selectModel`, sends the work order as a team message, then reads the
132
+ delivery back from the member's session events.
133
+
134
+ Members therefore show up in the team area and can change model in their own
135
+ session. Every step depends on host team services, so **any failure falls back to
136
+ the plain activation path** and the board's `teamMode` field says which one ran.
137
+ Known trade-offs:
138
+
139
+ - Member names must be ASCII (`lower-kebab-case`), so Chinese contractor names all
140
+ become `worker`, `worker-2`, … — the Chinese name and model live in the member
141
+ description, which the team area shows.
142
+ - A member's first turn runs on the parent's model (the model can only be switched
143
+ after the session exists — hence the one-line handshake).
144
+ - Switching is bounded by the host's per-subagent model allow-list; values outside
145
+ it are refused and the board shows `model switch: failed · reason`.
146
+ - Members stay in the team after the round.
147
+
148
+
149
+ ## License
150
+
151
+ MIT
package/README.zh.md ADDED
@@ -0,0 +1,188 @@
1
+ # 包工头 · dsh-agent-foreman
2
+
3
+ > 父智能体派单,子智能体干活,父智能体验收。
4
+ > 不预置角色、不写剧本:**谁干活、用什么模型,由你把关**。
5
+
6
+ ## 它解决的问题
7
+
8
+ DSH 自带的「智能体团队」只能给每个成员一个名字和一段提示词——**成员用哪个模型选不了**。
9
+ 而实际干活时,同一批任务里既有需要贵模型把关的部分,也有随手就能做的小改,全部继承
10
+ 父会话的模型既费钱也慢。
11
+
12
+ 包工头把这件事拆成两半:
13
+
14
+ - **承包商(contractor)**:你在设置页里定义的人,名字只是个标签,真正决定"谁干活"的是
15
+ `provider + 模型 + 思考等级`。想按专长分工就多雇几个实例、各起各的名字:便宜的配便宜模型
16
+ 跑搬砖活,贵的专门挑错,还有一个直接跟父会话同模型。每个承包商还能带一段**自己的提示词**
17
+ (设置页里点名字后面的 👁 编辑),派单时原样塞进它的工单里。
18
+ - **工单(order)**:父智能体拆出来的活。每张工单指定任务与验收标准,可以点名某个承包商,
19
+ 也可以自动轮转分配。
20
+
21
+ 父智能体只做两件事:拆单、验收。中间的过程全部由承包商在各自独立的子会话里完成,
22
+ 产物回收成一块共享看板。
23
+
24
+ ## 装上它
25
+
26
+ 把仓库放到任意目录,然后在 DSH 里:
27
+
28
+ ```bash
29
+ dsh plugin add E:/path/to/dsh-agent-foreman
30
+ ```
31
+
32
+ 或者手动安装(dev 用法):
33
+
34
+ 1. 把整个目录拷到 `<profile>/node_modules/dsh-agent-foreman`
35
+ 2. 在 `<profile>/package.json` 的 `dsh.profile.bundles` 里加上 `"dsh-agent-foreman"`
36
+ 3. 重启 DSH
37
+
38
+ 装好后打开 **设置 → 包工头**,添加承包商。
39
+
40
+ ## 用起来
41
+
42
+ ```
43
+ 1. 设置 → 包工头 → 添加承包商(名字、provider、模型、思考等级、并发各一格,👁 写提示词)
44
+ 力工-前端 trae deepseek-v4.1-flash 默认 开 1
45
+ 力工-渲染 workbuddy-global deepseek-v4.1-flash high 开 2
46
+ 安全员 deepseek-official deepseek-flash 默认 关 1
47
+
48
+ 分工靠名字:**包工头(父模型)拆单、力工干活、安全员挑错(可选)**。名册里只放你雇的人,
49
+ 包工头就是驱动这一切的父智能体,不占名册。
50
+
51
+ **想要专长就多雇几个实例**(两个力工各用各的模型、各写各的提示词)。派单时 `contractor`
52
+ 写名字就点某一个人,省略就全名册轮转。
53
+ 每个实例还带一个并发上限:默认 1,也就是同一个人串行干、不会自己跟自己抢文件;
54
+ 调成 2 它就能同时接两张单。
55
+
56
+ **最简形态只有一个人:力工。** 所有工单都给它,靠它的并发上限和策略里的总并发控制节奏。
57
+
58
+ 2. 在对话里直接说:"用包工头把这三件事分下去:……";或者用 `/foreman` 一句话派单(见下)
59
+
60
+ 3. 父智能体调用 foreman_dispatch 派单 → 等全部落定 → 拿回看板 → 逐单打分
61
+ ```
62
+
63
+ ### `/foreman`:一句话派单
64
+
65
+ 插件在宿主里注册了一个运行时 skill(`lib/skill.js`,`ctx.skills.register()`)。输入框里敲 `/`
66
+ 能看到 **foreman**,选中或直接打 `/foreman` 加任务即可:
67
+
68
+ ```
69
+ /foreman 把 GraphBlocked 抽成共享逻辑;给新逻辑补回归测试
70
+ ```
71
+
72
+ 分号或换行分段,一段一张工单。skill 正文规定四步:看名册 → 拆单(补齐背景/文件/验收)→
73
+ 一次 `foreman_dispatch` → 逐单 `foreman_review`。这条路径不靠常驻系统提示词,只在用到时
74
+ 注入一次,比每轮都挂着一段守则省。
75
+
76
+ 父智能体手里的四个工具:
77
+
78
+ | 工具 | 作用 |
79
+ | --- | --- |
80
+ | `foreman_roster` | 看名册:有哪些承包商、什么模型、提示词多少字、并发几张单、当前策略 |
81
+ | `foreman_dispatch` | 派一轮工单,等全部落定,返回看板(含每单交付摘要) |
82
+ | `foreman_review` | 逐单 pass / fail;fail 且还有额度就带着反馈重派给同一个人 |
83
+ | `foreman_status` | 回看某一轮看板;省略 runId 取本会话最近一轮 |
84
+
85
+ ### 成员在哪看
86
+
87
+ 不再自建聊天浮层了:契约商在团队模式下本身就是宿主的「智能体团队」成员,
88
+ 点会话头的 `智能体团队` 就能看到谁在、各自什么模型。设置页仍保留完整的
89
+ 「承包人岗位 + 模型 + 提示词 + 并发」编辑。
90
+
91
+ ### 名册要点
92
+
93
+ - 承包商 = 名字 + 提示词 + provider + 模型 + 思考等级 + 并发上限。**provider 选「跟随当前会话」时,
94
+ 子智能体直接跑父会话当前的模型**,不需要知道它是什么。
95
+ - 想换模型就分两步:先选 provider,**模型下拉只列这个 provider 自己的模型**(由 provider 推出来,
96
+ 没有模型的 provider 不会出现在下拉里)。
97
+ - 设置页把策略收进了「派单策略」折叠区,名册只留 名字 / provider / 模型 / 思考等级 / 并发 /
98
+ 启用,点名字后面的 👁 编辑这个承包商的提示词(派单时塞进工单,和工单冲突时以工单为准)。
99
+
100
+ ![设置页的名册](docs/settings-roster.png)
101
+
102
+ ### 派单参数要点
103
+
104
+ - `orders[].task` **必须自足**:子智能体看不到父会话的任何上下文,背景、要动的文件、
105
+ 期望结果都要写进去。
106
+ - `orders[].contractor` 写承包商名字;省略则全名册轮转;点名不存在的人会直接报错并列出可选名字。
107
+ - `orders[].dependsOn` 只能引用本轮内的工单;有依赖的工单会排在后面串行跑。
108
+ - `waitSeconds` 是这一轮的总墙钟上限,到点会中止没跑完的工单并把现状返回——不会让父智能体
109
+ 拿到一份假的"全部完成"。
110
+ - `concurrency` 默认 4,上限 8。
111
+
112
+ ### 验收与返工
113
+
114
+ `foreman_review` 打回时,反馈会原样进入下一次派单的提示词("上一轮被打回,原因:……")。
115
+ 返工额度由策略里的 `maxRework` 控制(默认 1 次),额度用完后打回只会记录结论、不再重派,
116
+ 这一轮转入 `closed`。
117
+
118
+ ## 策略
119
+
120
+ 设置页可调,也可以直接改数据文件里的 `policy`:
121
+
122
+ | 字段 | 默认 | 说明 |
123
+ | --- | --- | --- |
124
+ | `concurrency` | 4 | 同时在场的承包商数(1–8);单人的上限在名册里按人设 |
125
+ | `timeoutSeconds` | 900 | 单张工单的墙钟上限 |
126
+ | `maxRework` | 1 | 每张工单最多被返工几次 |
127
+ | `maxDepth` | 1 | 子智能体还能不能再派子智能体 |
128
+ | `provider` | `spawn` | 走哪个 subagents provider(`spawn` / `fork` / `cursor`) |
129
+
130
+ ## 数据放在哪
131
+
132
+ - Windows:`%LOCALAPPDATA%/dsh-agent-foreman/`
133
+ - 其它平台:`<baseDir>/data/dsh-agent-foreman/`
134
+
135
+ 里面两个文件:`roster.json`(名册与策略)、`runs.jsonl`(每轮台账)。
136
+ 坏了不会阻断启动:读不出来的名册会被当成空名册并记一条日志。
137
+
138
+ ## 安全边界
139
+
140
+ - 所有 HTTP 接口只接受同源、且落在回环地址上的请求;跨站请求一律 403(有 CSRF 面)。
141
+ - 名册只保存 `provider` / `model` / 思考等级等配置,不保存任何密钥。
142
+ - 承包商在独立子会话里跑,沙箱与权限沿用 DSH 当前生效的那一套。
143
+
144
+ ## 开发
145
+
146
+ ```bash
147
+ node --test test/*.test.mjs # 37 条用例:工具、引擎、路由、CSRF、台账、并发上限
148
+ ```
149
+
150
+ 测试用假宿主跑真实的插件半边(`test/harness.test.mjs`),不需要启动 DSH。
151
+
152
+ ## 团队模式(实验)
153
+
154
+ 默认关闭。在「设置 → 包工头 → 派单策略」勾上**团队模式**后,承包商不再走
155
+ `subagents.startActivation`,而是:
156
+
157
+ 1. `agentTeams.spawnTeammate` 把它加成宿主的**智能体团队**成员(自己的会话);
158
+ 2. `sessionController.selectModel` 把成员会话的模型换成名册里配的那套;
159
+ 3. `agentTeams.sendMessage` 把工单当团队消息发过去;
160
+ 4. 等它那一轮跑完,从它的会话事件里把交付正文捞回来进看板。
161
+
162
+ 这样承包商既能在「智能体团队」区看见,也能在它自己的会话里换模型。
163
+
164
+ 这条链路依赖宿主的团队服务,因此**任何一步不可用都会自动退回普通模式**,
165
+ 看板每单的 `teamMode` 字段会写明是「团队模式」还是「失败退回」。已知取舍:
166
+
167
+ - 成员名只能是 ASCII(宿主只收 `lower-kebab-case`),所以中文承包商统一落成
168
+ `worker`、`worker-2`…,中文名与模型写在成员的描述里;
169
+ - 成员第一轮用的是父会话的模型(建成员时给不了模型),切模型从第二轮才生效
170
+ ——所以建成员时只发一句「待命」,工单随后再发;
171
+ - 切模型受宿主给子会话的**模型白名单**限制,白名单里没有的 provider/模型会被拒,
172
+ 这种单在看板上会写明「模型切换:failed · 原因」;
173
+ - 每轮派单结束时成员会留在团队里(可继续用,也可手动删除);
174
+ - 并发上限仍按名册的「并发」和策略的「同时在场承包商」卡。
175
+
176
+ ## 与其它方案的区别
177
+
178
+ | | 智能体团队(内置) | swarm-orchestrator | 包工头 |
179
+ | --- | --- | --- | --- |
180
+ | 成员可选模型 | ✗ | ✗ | **✓** |
181
+ | 角色预设 | 固定 | 4 个固定角色 | 不预置,你定义 |
182
+ | 共享看板 | ✓ | ✓ | ✓ |
183
+ | 返工闭环 | ✗ | 有限 | ✓ 带反馈重派 |
184
+ | 设置页 | ✓ | ✗ | ✓ 名册 + 模型目录 |
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,11 @@
1
+ # 包工头(dsh-agent-foreman)
2
+ #
3
+ # 只插一行:整块能力都长在这一个插件上——名册、派单引擎、看板、设置页都由
4
+ # lib/index.js 挂载。分包商(承包商)是运行时数据,不是 cordis 插件行,所以
5
+ # 这里没有第二个 id。
6
+ #
7
+ # 客户端半边(设置页里的名册编辑器)由 package.json 的 dsh.client 声明,
8
+ # 宿主自动加载 lib/client.js,无需在此重复。
9
+ - insert:
10
+ - id: dsh-agent-foreman
11
+ name: dsh-agent-foreman
Binary file
package/icon.svg ADDED
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Foreman">
2
+ <rect width="64" height="64" rx="14" fill="#1f2937"/>
3
+ <path d="M12 40h40v5a3 3 0 0 1-3 3H15a3 3 0 0 1-3-3v-5z" fill="#f59e0b"/>
4
+ <path d="M18 40V30a14 14 0 0 1 28 0v10H18z" fill="#fbbf24"/>
5
+ <path d="M28 18h8v22h-8z" fill="#fde68a"/>
6
+ <path d="M30.5 20h3v20h-3z" fill="#f59e0b" opacity=".55"/>
7
+ <circle cx="32" cy="47" r="2.6" fill="#1f2937"/>
8
+ </svg>
package/lib/catalog.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * 包工头 · 模型目录读取。
3
+ *
4
+ * 承包商的名字只是标签,真正决定"谁干活"的是 provider + 模型。设置页要能下拉
5
+ * 出宿主机真实在用的目录,所以这里直接问 `llm` 服务,不维护第二份清单。
6
+ *
7
+ * 三个已知的宿主约束,这里的代码都绕开了:
8
+ *
9
+ * 1. `listModels()` 不带推理等级——等级只在 `resolveModelInfo()` 里,而且那是
10
+ * 一次按模型的异步查询。所以等级走单独端点,等用户选了模型才查。
11
+ * 2. 某个 provider 的目录可能整个失败(没配 key、网关不通)。一个 provider 挂掉
12
+ * 不应该让整个下拉框变空,因此逐个捕获,把错误挂在该 provider 上返回。
13
+ * 3. `llm` 可能根本不存在(无头 profile)。这时返回空目录而不是抛错,设置页会
14
+ * 退化成手填输入框。
15
+ *
16
+ * @module dsh-agent-foreman/catalog
17
+ */
18
+
19
+ /**
20
+ * 取 LLM 服务;缺失时返回 undefined(`get()` 不会因未 inject 而抛)。
21
+ * @param ctx - cordis 上下文。
22
+ */
23
+ function llmOf(ctx) {
24
+ try {
25
+ const llm = ctx.get('llm');
26
+ return typeof llm?.listProviders === 'function' ? llm : undefined;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 列出宿主当前注册的 provider。
35
+ * @param ctx - cordis 上下文。
36
+ * @returns `{ id, name }[]`,缺失服务时为空数组。
37
+ */
38
+ export function readProviders(ctx) {
39
+ const llm = llmOf(ctx);
40
+ if (llm === undefined)
41
+ return [];
42
+ try {
43
+ const providers = llm.listProviders();
44
+ return Array.isArray(providers)
45
+ ? providers.map(provider => ({ id: String(provider.id), name: String(provider.name ?? provider.id) }))
46
+ : [];
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 读取完整目录:每个 provider 及其模型。
55
+ *
56
+ * 串行而不是并发,因为适配器的 `listModels` 大多要打一次远端接口,而目录读取
57
+ * 只在打开设置页时发生一次——并发省不下多少时间,却会把某个坏网关的错误放大
58
+ * 成一片超时。
59
+ *
60
+ * @param ctx - cordis 上下文。
61
+ * @returns `{ providers: [{ id, name, models, error? }], fetchedAt }`。
62
+ */
63
+ export async function readCatalog(ctx) {
64
+ const llm = llmOf(ctx);
65
+ if (llm === undefined)
66
+ return { providers: [], fetchedAt: new Date().toISOString(), unavailable: true };
67
+ const providers = readProviders(ctx);
68
+ const out = [];
69
+ for (const provider of providers) {
70
+ try {
71
+ const models = await llm.listModels(provider.id);
72
+ out.push({
73
+ ...provider,
74
+ models: Array.isArray(models)
75
+ ? models.map(model => ({ id: String(model.id), name: String(model.name ?? model.id) }))
76
+ : [],
77
+ });
78
+ }
79
+ catch (error) {
80
+ out.push({ ...provider, models: [], error: error instanceof Error ? error.message : String(error) });
81
+ }
82
+ }
83
+ return { providers: out, fetchedAt: new Date().toISOString() };
84
+ }
85
+
86
+ /**
87
+ * 读取单个模型的推理等级。等级随模型而异,不能拿父会话的等级硬套。
88
+ * @param ctx - cordis 上下文。
89
+ * @param provider - provider id。
90
+ * @param model - 模型 id。
91
+ * @returns `{ efforts: [{ id, name }], defaultEffort? }`;查不到时为空。
92
+ */
93
+ export async function readEfforts(ctx, provider, model) {
94
+ const llm = llmOf(ctx);
95
+ if (llm === undefined || typeof llm.resolveModelInfo !== 'function')
96
+ return { efforts: [] };
97
+ try {
98
+ const info = await llm.resolveModelInfo(provider, model, undefined);
99
+ const efforts = Array.isArray(info?.reasoning?.efforts)
100
+ ? info.reasoning.efforts.map(effort => ({ id: String(effort.id), name: String(effort.name ?? effort.id) }))
101
+ : [];
102
+ const defaultEffort = info?.reasoning?.defaultEffort;
103
+ return typeof defaultEffort === 'string' ? { efforts, defaultEffort } : { efforts };
104
+ }
105
+ catch {
106
+ return { efforts: [] };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * 宿主实际注册的 subagents provider 名。
112
+ *
113
+ * `SubagentRuntime.providers` 是公开的 Map(审计工具也这么读),但服务可能尚未
114
+ * 就绪;读不到就返回空数组,调用方据此决定是否回退到配置里的默认名。
115
+ *
116
+ * @param ctx - cordis 上下文。
117
+ * @returns provider 名数组。
118
+ */
119
+ export function readSubagentProviders(ctx) {
120
+ try {
121
+ const service = ctx.get('subagents');
122
+ const providers = service?.providers;
123
+ if (providers instanceof Map)
124
+ return [...providers.keys()].map(String);
125
+ if (typeof service?.getProvider === 'function') {
126
+ const names = [];
127
+ for (const candidate of ['spawn', 'cursor', 'acp', 'external'])
128
+ if (service.getProvider(candidate) !== undefined)
129
+ names.push(candidate);
130
+ return names;
131
+ }
132
+ return [];
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }