@menghuan1918/dsh-apollo 0.4.0 → 0.5.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/README.md +135 -131
- package/cordis.patch.yml +75 -42
- package/docs/DEVELOPMENT.md +281 -470
- package/package.json +8 -5
- package/presets/eng/plugins/exit-guard/index.js +39 -51
- package/presets/eng/plugins/mode-instructions/index.js +23 -59
- package/presets/eng-ptc.patch.yml +5 -9
- package/presets/eng.patch.yml +29 -36
- package/src/client.js +63 -912
- package/src/config.js +31 -10
- package/src/index.js +13 -7
- package/src/migrate.js +602 -0
- package/src/owns-host.js +66 -0
- package/src/settings.js +39 -35
- package/presets/eng/plugins/wait-subagent/index.js +0 -233
- package/presets/eng/plugins/wait-subagent/package.json +0 -7
- package/src/cli/claude.js +0 -166
- package/src/cli/codex.js +0 -164
- package/src/cli/jsonl.js +0 -216
- package/src/cli-adapter.js +0 -198
- package/src/cli-sessions.js +0 -69
- package/src/panel.js +0 -844
- package/src/subagents.js +0 -198
package/src/settings.js
CHANGED
|
@@ -1,46 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-apollo —— 设置装配(src/settings.js)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 0.5.0 起 Apollo 不再注册任何 HTTP 路由:子代理能力/模型/路由探针与
|
|
5
|
+
* `/apollo/api/*` 别名由 dsh-better-subagent 拥有,compact 诊断由
|
|
6
|
+
* dsh-better-compact 拥有。重复注册同一 `(kind, path)` 会让 webServer 抛错,
|
|
7
|
+
* 所以这里必须保持“零路由”。
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* 本文件只做三件需要等服务的事情:
|
|
10
|
+
*
|
|
11
|
+
* 1. **反代部署标志**:`installOwnsHostTap`(`src/owns-host.js`)在行配置
|
|
12
|
+
* `declareOwnsHost: true` 时注入 index boot 全局。
|
|
10
13
|
*
|
|
11
14
|
* 2. **行配置页面策略**:`ctx.inject(['settings'])` → 在本插件实例上登记
|
|
12
|
-
* `configure({ auto: false }, ctx.fiber)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* `configure({ auto: false }, ctx.fiber)`:本行自带的最小配置页就是唯一
|
|
16
|
+
* 编辑入口,不需要 DSH 再按 schema 自动生成一页。策略随本行的 effect
|
|
17
|
+
* 生命周期增删。
|
|
15
18
|
*
|
|
16
19
|
* 3. **旧 eng.json / atlas.json 一次性只读导入**:DSH 0.2 起设置就是正式
|
|
17
|
-
* Config(volatile,见 `src/config.js
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Config(volatile,见 `src/config.js`)。`<dshHome>/eng.json` 与旧名
|
|
21
|
+
* `atlas.json` 不再被写入(**只读**),但第一次装配时会把里面的
|
|
22
|
+
* `exitGuard` 搬进正式配置。0.5.0 后这个 `exitGuard` 是 **job/通用守卫**
|
|
23
|
+
* 的开关;同一个初值再由迁移模块复制到 dsh-better-subagent 的
|
|
24
|
+
* `childExitGuard`(旧组合开关拆成两个可独立配置的开关)。
|
|
21
25
|
*
|
|
22
26
|
* 迁移规则:
|
|
23
|
-
* - 只在 `settings` 服务可用、且 Loader 已把所有 entry 装配完(本行
|
|
24
|
-
* 进入 active,`describe()` 才认得它)之后跑一次;
|
|
25
|
-
* - **当前 profile 显式给出的值优先**:`exitGuard`
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - 值已经与生效值相同(等于 schema 默认)时不写盘,避免制造无意义的
|
|
29
|
-
* profile 覆盖层;
|
|
27
|
+
* - 只在 `settings` 服务可用、且 Loader 已把所有 entry 装配完(本行
|
|
28
|
+
* fiber 进入 active,`describe()` 才认得它)之后跑一次;
|
|
29
|
+
* - **当前 profile 显式给出的值优先**:`exitGuard` 已存在于用户层(哪怕
|
|
30
|
+
* 是显式 `false`)时不覆盖;
|
|
31
|
+
* - 值已经与生效值相同(等于 schema 默认)时不写盘;
|
|
30
32
|
* - 旧文件永不改写、永不删除;导入失败只告警,不影响插件装载。
|
|
31
33
|
*
|
|
32
34
|
* 持久化与生效:写入经 `settings.update`(= configEditor 校验完整候选后落盘 +
|
|
33
|
-
* Loader 调和)。因为 `exitGuard` 是 volatile
|
|
34
|
-
* 消费方(预设守卫、CLI 路由)下一次读取即拿到新值。
|
|
35
|
+
* Loader 调和)。因为 `exitGuard` 是 volatile 字段,写入不会重挂本行。
|
|
35
36
|
*
|
|
36
|
-
* 零第三方依赖:只用 node 内置模块 + 本包 `src/config.js`。
|
|
37
|
+
* 零第三方依赖:只用 node 内置模块 + 本包 `src/config.js` / `src/owns-host.js`。
|
|
37
38
|
*/
|
|
38
39
|
|
|
39
40
|
import { readFileSync } from 'node:fs'
|
|
40
41
|
import { join } from 'node:path'
|
|
41
42
|
import { ENTRY_ID } from './config.js'
|
|
42
43
|
import { resolveDshHome } from './dsh-home.js'
|
|
43
|
-
import { installOwnsHostTap
|
|
44
|
+
import { installOwnsHostTap } from './owns-host.js'
|
|
44
45
|
|
|
45
46
|
/** 旧配置文件名,按优先级排列(新名优先,旧名只读回退)。 */
|
|
46
47
|
export const LEGACY_FILES = Object.freeze(['eng.json', 'atlas.json'])
|
|
@@ -126,13 +127,13 @@ function loaderOf(ctx) {
|
|
|
126
127
|
* 调用方保证「只调用一次」;本函数自身也幂等(用户层已有该字段就不写)。
|
|
127
128
|
* @param ctx - 已注入 `settings` 的上下文。
|
|
128
129
|
* @param env - 读取 DSH_HOME 的进程环境(测试注入点)。
|
|
129
|
-
* @returns `{ imported, reason, file?, patch? }
|
|
130
|
+
* @returns `{ imported, reason, file?, patch? }`(不抛错以外的失败信息只用于日志)。
|
|
130
131
|
*/
|
|
131
132
|
export async function importLegacyConfig(ctx, env = process.env) {
|
|
132
133
|
const legacy = readLegacyDocument(env)
|
|
133
134
|
if (legacy === undefined) return { imported: false, reason: 'absent' }
|
|
134
135
|
const settings = ctx.settings
|
|
135
|
-
if (typeof settings?.describe !== 'function' || typeof settings
|
|
136
|
+
if (typeof settings?.describe !== 'function' || typeof settings?.update !== 'function') {
|
|
136
137
|
return { imported: false, reason: 'unavailable', file: legacy.file }
|
|
137
138
|
}
|
|
138
139
|
// Loader 装配完成后 describe() 才认得出本行(fiber 必须 active)。
|
|
@@ -150,25 +151,24 @@ export async function importLegacyConfig(ctx, env = process.env) {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
/**
|
|
153
|
-
*
|
|
154
|
+
* 装配设置侧:安装反代部署 tap、登记行配置页面策略、一次性导入旧配置。
|
|
154
155
|
*
|
|
155
156
|
* 由宿主行(`src/index.js` 的 `apply`)调用。所有动作都延迟到服务出现时进行:
|
|
156
157
|
* 没有 webServer / settings 的组合里本函数安静返回,插件其余部分照常工作。
|
|
157
158
|
* @param ctx - 插件上下文。
|
|
158
159
|
* @param config - 行配置(可能含 volatile 引用)。
|
|
159
|
-
* @param options - 注入点(测试用):`env
|
|
160
|
-
*
|
|
160
|
+
* @param options - 注入点(测试用):`env`。
|
|
161
|
+
* @returns `{ legacyImport }`:旧配置导入结束时 resolve 的 Promise(失败也
|
|
162
|
+
* resolve,错误只记日志)。迁移模块用它保证先导入、再读取来源。
|
|
161
163
|
*/
|
|
162
164
|
export function mountSettings(ctx, config, options = {}) {
|
|
163
165
|
const env = options.env ?? process.env
|
|
164
166
|
|
|
165
|
-
// 反代部署标志:宿主行只调 mountSettings
|
|
167
|
+
// 反代部署标志:宿主行只调 mountSettings 时也必须装上。
|
|
166
168
|
installOwnsHostTap(ctx, config)
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
171
|
-
|
|
170
|
+
let settle
|
|
171
|
+
const legacyImport = new Promise((resolve) => { settle = resolve })
|
|
172
172
|
let started = false
|
|
173
173
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
174
174
|
// 行配置页面策略:本行自带配置页,不需要自动生成的一页。
|
|
@@ -178,7 +178,10 @@ export function mountSettings(ctx, config, options = {}) {
|
|
|
178
178
|
} catch (error) {
|
|
179
179
|
settingsCtx.logger?.warn?.(`apollo-settings: settings page policy was not registered: ${error instanceof Error ? error.message : String(error)}`)
|
|
180
180
|
}
|
|
181
|
-
if (started)
|
|
181
|
+
if (started) {
|
|
182
|
+
settle()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
182
185
|
started = true
|
|
183
186
|
void importLegacyConfig(settingsCtx, env).then((result) => {
|
|
184
187
|
if (result.imported) {
|
|
@@ -186,6 +189,7 @@ export function mountSettings(ctx, config, options = {}) {
|
|
|
186
189
|
}
|
|
187
190
|
}, (error) => {
|
|
188
191
|
settingsCtx.logger?.warn?.(`apollo-settings: legacy config import skipped: ${error instanceof Error ? error.message : String(error)}`)
|
|
189
|
-
})
|
|
192
|
+
}).finally(settle)
|
|
190
193
|
})
|
|
194
|
+
return { legacyImport }
|
|
191
195
|
}
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `wait_subagent` tool — explicit wait for continuable children to settle.
|
|
3
|
-
*
|
|
4
|
-
* Preset-local plugin for the eng preset (resolved relative to this preset's
|
|
5
|
-
* directory). Continuable subagents live in the `subagents` registry, not the
|
|
6
|
-
* `jobs` registry, so the only settlement signal is the scoped `subagent/end`
|
|
7
|
-
* event. The tool takes an ARRAY of durable child ids (`subagent_id`), awaits
|
|
8
|
-
* them all concurrently (duplicates deduped, order preserved), and returns one
|
|
9
|
-
* SHORT line per id (done + stopReason).
|
|
10
|
-
*
|
|
11
|
-
* The children's content is none of this tool's business: the subagent manager
|
|
12
|
-
* unconditionally delivers a settlement notice to the parent BEFORE
|
|
13
|
-
* `subagent/end` fires (dispose order: notifySettlement() →
|
|
14
|
-
* observer.settle()), carrying the one-line account and the closing message.
|
|
15
|
-
* A busy parent is steered, so the notice sits in the pending next-step queue
|
|
16
|
-
* and reaches the model right after this tool result — exactly once, per
|
|
17
|
-
* child, by the framework. Earlier designs returned the child's final message
|
|
18
|
-
* here AND spliced the duplicate notice (and any mid-run `report`) out of the
|
|
19
|
-
* parent inbox; that coupled this plugin to `agent.inbox` internals and to
|
|
20
|
-
* the `subagent-report` source kind, which upstream removed in
|
|
21
|
-
* 0.1.2-alpha.4 (bidirectional `send_message` replaced `report`). Returning
|
|
22
|
-
* "done" lines and leaving delivery to the framework has no such coupling.
|
|
23
|
-
* Mid-run messages the child sends (`send_message`, source kind
|
|
24
|
-
* `agent-message`) likewise stay in place.
|
|
25
|
-
*
|
|
26
|
-
* `timeout_ms` is ONE shared window over the whole batch (all timers start at
|
|
27
|
-
* the same instant), matching the semantics same-message parallel calls had.
|
|
28
|
-
* Values below MIN_TIMEOUT_MS (15 minutes) are raised to it: short polling
|
|
29
|
-
* waits only burned turns, since a finishing child notifies the parent anyway.
|
|
30
|
-
* An id that is not a direct child fails the whole call fast — nobody is
|
|
31
|
-
* waited for — keeping the contract the single-id version had. A child
|
|
32
|
-
* dispatched moments ago may not have started its first turn yet: its status
|
|
33
|
-
* is not `running` and no settlement exists. Returning early with a "not
|
|
34
|
-
* running" note there (the pre-grace behavior) ended the turn before the
|
|
35
|
-
* child even started and forced a re-wait on the next turn — session audit
|
|
36
|
-
* 2f5dd618. Instead, a not-yet-started child gets a startup grace window
|
|
37
|
-
* (START_GRACE_MS): the tool keeps waiting while the child spins up,
|
|
38
|
-
* settles, the optional timeout elapses, or the grace expires without the
|
|
39
|
-
* child ever starting (only then a retryable informational line).
|
|
40
|
-
*
|
|
41
|
-
* Zero imports on purpose: the module loads from the preset directory, where
|
|
42
|
-
* Node's upward node_modules walk never reaches the harness install, so bare
|
|
43
|
-
* package imports would fail. Everything needed comes from the context.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
export const name = 'tool-wait-subagent'
|
|
47
|
-
|
|
48
|
-
export const inject = ['tools', 'subagents', 'agents']
|
|
49
|
-
|
|
50
|
-
/** Grace window for a just-dispatched child that has not started running yet. */
|
|
51
|
-
const START_GRACE_MS = 30_000
|
|
52
|
-
|
|
53
|
-
/** Floor for a caller-supplied timeout_ms (15 minutes). */
|
|
54
|
-
const MIN_TIMEOUT_MS = 15 * 60_000
|
|
55
|
-
|
|
56
|
-
/** Status polling interval while waiting for a not-yet-started child. */
|
|
57
|
-
const POLL_MS = 250
|
|
58
|
-
|
|
59
|
-
export function apply(ctx) {
|
|
60
|
-
/** childId -> { time, info } — latest settlement observed since mount. */
|
|
61
|
-
const settled = new Map()
|
|
62
|
-
/** childId -> finish(info) callbacks awaiting settlement. */
|
|
63
|
-
const waiters = new Map()
|
|
64
|
-
|
|
65
|
-
ctx.on('subagent/end', (info) => {
|
|
66
|
-
const id = info && info.id
|
|
67
|
-
if (typeof id !== 'string') return
|
|
68
|
-
settled.set(id, { time: Date.now(), info })
|
|
69
|
-
const list = waiters.get(id)
|
|
70
|
-
if (list !== undefined) {
|
|
71
|
-
waiters.delete(id)
|
|
72
|
-
for (const finish of [...list]) finish({ kind: 'settled', info })
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
ctx.tools.register({
|
|
77
|
-
name: 'wait_subagent',
|
|
78
|
-
description: 'Wait for direct subagents (from subagent or subagent_fork) to finish their current turn. Pass every id in subagent_id; all are awaited together and each gets one done line, while its closing message arrives as the settlement notice right after. Use it when your next step needs the results, instead of messaging the child to check on it. Not for bash jobs (use job_output).',
|
|
79
|
-
parameters: {
|
|
80
|
-
type: 'object',
|
|
81
|
-
properties: {
|
|
82
|
-
subagent_id: {
|
|
83
|
-
type: 'array',
|
|
84
|
-
items: { type: 'string' },
|
|
85
|
-
minItems: 1,
|
|
86
|
-
description: 'Durable ids returned by subagent or subagent_fork.',
|
|
87
|
-
},
|
|
88
|
-
timeout_ms: {
|
|
89
|
-
type: 'integer',
|
|
90
|
-
description: 'Optional shared wait window in ms; values below 900000 (15 minutes) are raised to 900000. Omit to wait until every child settles.',
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
required: ['subagent_id'],
|
|
94
|
-
},
|
|
95
|
-
output: {
|
|
96
|
-
schema: { type: 'string' },
|
|
97
|
-
render: (_args, value) => [{ type: 'text', text: value }],
|
|
98
|
-
},
|
|
99
|
-
isConcurrencySafe: () => true,
|
|
100
|
-
async execute(args, exec) {
|
|
101
|
-
// Dedupe preserving input order; the schema already enforces a
|
|
102
|
-
// non-empty array, the guard below only covers malformed direct calls.
|
|
103
|
-
const ids = [...new Set(Array.isArray(args.subagent_id) ? args.subagent_id : [])]
|
|
104
|
-
if (ids.length === 0) return 'subagent_id must be a non-empty array of subagent ids.'
|
|
105
|
-
const parent = exec.agent
|
|
106
|
-
if (parent === undefined) throw new Error('wait_subagent requires a calling agent (exec.agent was undefined)')
|
|
107
|
-
|
|
108
|
-
// Verify every id is one of this agent's direct children — fail fast,
|
|
109
|
-
// waiting for nobody (the contract the single-id version had).
|
|
110
|
-
let entries
|
|
111
|
-
try {
|
|
112
|
-
entries = await ctx.subagents.listChildren(parent.id, exec.signal)
|
|
113
|
-
} catch (error) {
|
|
114
|
-
return 'listChildren failed: ' + (error && error.message ? error.message : String(error))
|
|
115
|
-
}
|
|
116
|
-
const known = new Set(entries.map((entry) => entry.id))
|
|
117
|
-
const unknown = ids.filter((id) => !known.has(id))
|
|
118
|
-
if (unknown.length > 0) {
|
|
119
|
-
return `unknown subagent ${unknown.map((id) => `"${id}"`).join(', ')}: not a direct child of this agent (list_agents shows your children; only depth-1 children can be awaited).`
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const statusOf = (id) => {
|
|
123
|
-
const agent = ctx.agents.get(id)
|
|
124
|
-
return agent === undefined ? 'ready' : agent.status === 'running' ? 'running' : 'idle'
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** Settlement line: short done line; content belongs to the framework notice. */
|
|
128
|
-
const doneLine = (id, info) => `subagent ${id} done (${info && info.stopReason ? info.stopReason : 'completed'}); its closing message follows as the settlement notice.`
|
|
129
|
-
|
|
130
|
-
const startedAt = Date.now()
|
|
131
|
-
const capMs = typeof args.timeout_ms === 'number' && args.timeout_ms > 0 ? Math.max(args.timeout_ms, MIN_TIMEOUT_MS) : undefined
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Wait for one child. Resolves with {kind:'settled',info} |
|
|
135
|
-
* {kind:'timedOut'} | {kind:'aborted'} | {kind:'notStarted'}. The
|
|
136
|
-
* per-id machinery (settled fast path, waiter registration, timeout
|
|
137
|
-
* timer, startup-grace poller) is the single-id logic of old,
|
|
138
|
-
* unchanged; Promise.all over ids gives the concurrent batch.
|
|
139
|
-
*/
|
|
140
|
-
const waitOne = (id) => {
|
|
141
|
-
// Fast path: the child is not running and a settlement is already on
|
|
142
|
-
// record — resolve immediately (the notice was already delivered when
|
|
143
|
-
// the child settled, so it is in the conversation history).
|
|
144
|
-
if (statusOf(id) !== 'running') {
|
|
145
|
-
const hit = settled.get(id)
|
|
146
|
-
if (hit !== undefined) return Promise.resolve({ kind: 'settled', info: hit.info })
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return new Promise((resolve) => {
|
|
150
|
-
let done = false
|
|
151
|
-
let timer
|
|
152
|
-
let poller
|
|
153
|
-
const finish = (value) => {
|
|
154
|
-
if (done) return
|
|
155
|
-
done = true
|
|
156
|
-
if (poller !== undefined) clearInterval(poller)
|
|
157
|
-
const list = waiters.get(id)
|
|
158
|
-
if (list !== undefined) {
|
|
159
|
-
const index = list.indexOf(finish)
|
|
160
|
-
if (index >= 0) list.splice(index, 1)
|
|
161
|
-
if (list.length === 0) waiters.delete(id)
|
|
162
|
-
}
|
|
163
|
-
if (timer !== undefined) clearTimeout(timer)
|
|
164
|
-
if (exec.signal !== undefined) exec.signal.removeEventListener('abort', onAbort)
|
|
165
|
-
resolve(value)
|
|
166
|
-
}
|
|
167
|
-
const onAbort = () => finish({ kind: 'aborted' })
|
|
168
|
-
|
|
169
|
-
const list = waiters.get(id) ?? []
|
|
170
|
-
list.push(finish)
|
|
171
|
-
waiters.set(id, list)
|
|
172
|
-
// All ids share startedAt, so one capMs value is one shared window.
|
|
173
|
-
if (capMs !== undefined) timer = setTimeout(() => finish({ kind: 'timedOut' }), capMs)
|
|
174
|
-
if (exec.signal !== undefined) {
|
|
175
|
-
if (exec.signal.aborted) {
|
|
176
|
-
finish({ kind: 'aborted' })
|
|
177
|
-
return
|
|
178
|
-
}
|
|
179
|
-
exec.signal.addEventListener('abort', onAbort, { once: true })
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Startup grace: a freshly dispatched child sits between spawn and
|
|
183
|
-
// its first turn (status not `running`, no settlement yet). Keep the
|
|
184
|
-
// wait alive while it spins up; once `running`, only `subagent/end`
|
|
185
|
-
// (the listener above) resolves us. If the grace expires without the
|
|
186
|
-
// child ever starting or settling, fall back to a retryable note
|
|
187
|
-
// instead of the old misleading "is not running" early return.
|
|
188
|
-
poller = setInterval(() => {
|
|
189
|
-
if (done) return
|
|
190
|
-
const hit = settled.get(id)
|
|
191
|
-
if (hit !== undefined) {
|
|
192
|
-
finish({ kind: 'settled', info: hit.info })
|
|
193
|
-
return
|
|
194
|
-
}
|
|
195
|
-
if (statusOf(id) !== 'running' && Date.now() - startedAt >= START_GRACE_MS) {
|
|
196
|
-
finish({ kind: 'notStarted' })
|
|
197
|
-
}
|
|
198
|
-
}, POLL_MS)
|
|
199
|
-
|
|
200
|
-
// Re-check: the child may have settled between the fast path above
|
|
201
|
-
// and waiter registration. Only a settlement AFTER this wait started
|
|
202
|
-
// counts as a live result.
|
|
203
|
-
const hit = settled.get(id)
|
|
204
|
-
if (hit !== undefined && hit.time >= startedAt) finish({ kind: 'settled', info: hit.info })
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const outcomes = await Promise.all(ids.map((id) => waitOne(id)))
|
|
209
|
-
|
|
210
|
-
// Every waiter shares one abort signal, so an interruption either
|
|
211
|
-
// aborts the whole batch or lands after some children already settled.
|
|
212
|
-
// Collapse the all-aborted case into one line; mixed cases keep
|
|
213
|
-
// per-id lines so the settled ones are not lost.
|
|
214
|
-
if (outcomes.length > 0 && outcomes.every((outcome) => outcome.kind === 'aborted')) {
|
|
215
|
-
return `wait for ${ids.length} subagent${ids.length === 1 ? '' : 's'} was aborted by user interruption; the children may still be running.`
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const lines = outcomes.map((outcome, index) => {
|
|
219
|
-
const id = ids[index]
|
|
220
|
-
if (outcome.kind === 'aborted') return `wait for subagent ${id} was aborted by user interruption; the child may still be running.`
|
|
221
|
-
if (outcome.kind === 'timedOut') return `timed out waiting for subagent ${id}; it is still running. You will be notified when it finishes — you can wait_subagent it again or continue with other work.`
|
|
222
|
-
if (outcome.kind === 'notStarted') {
|
|
223
|
-
return `subagent ${id} has not started after ${Math.round(START_GRACE_MS / 1000)}s (status: ${statusOf(id)}). It may still be spinning up — call wait_subagent again, or continue with other work; you will be notified when it finishes.`
|
|
224
|
-
}
|
|
225
|
-
// Real settlement: short done line; the framework's settlement notice
|
|
226
|
-
// (already delivered to the parent inbox before `subagent/end`)
|
|
227
|
-
// carries the closing message — nothing to consume or deduplicate.
|
|
228
|
-
return doneLine(id, outcome.info)
|
|
229
|
-
})
|
|
230
|
-
return lines.join('\n')
|
|
231
|
-
},
|
|
232
|
-
})
|
|
233
|
-
}
|
package/src/cli/claude.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code CLI 适配:纯命令构造 + `--print --output-format stream-json` 事件流解析。
|
|
3
|
-
* 只产出 argv 与 stdin 正文并翻译事件;spawn/管道/stderr/中断由父 adapter 的
|
|
4
|
-
* `ctx.subprocess` 掌握(stdin 取 `{data: input}`)。不 import 任何 DSH 包。
|
|
5
|
-
*
|
|
6
|
-
* 实测依据(Claude Code 2.1.300,新建 + `--resume` 各一轮真实跑通):
|
|
7
|
-
* - `--print` 不给位置参数 prompt 时正文从 stdin 读,所以 argv 里没有 prompt;
|
|
8
|
-
* - `stream-json` 必须配 `--verbose` 才是完整事件流;
|
|
9
|
-
* - `--permission-prompts none` = 没人回答权限提问、会被提问的一律自动拒绝,
|
|
10
|
-
* 正是无人值守管道要的语义(`--permission-mode` 仍决定其余判定);
|
|
11
|
-
* - 续接用 `--resume <SESSION_ID>`;**不传** `--no-session-persistence`,否则会话
|
|
12
|
-
* 不落盘、下一轮无法续接;
|
|
13
|
-
* - claude **没有** cwd 选项(`--add-dir` 只是追加可写目录),工作目录由进程 cwd
|
|
14
|
-
* 决定,故本模块接受并忽略 `cwd`,交给父 adapter 的 spawn `cwd`;`profile` 是
|
|
15
|
-
* codex 专属概念,同样忽略。
|
|
16
|
-
*
|
|
17
|
-
* 事件语义:
|
|
18
|
-
* - `system`(hook_started / hook_response / init)都带 `session_id`,且前两者出现在
|
|
19
|
-
* `init` **之前**——故 remoteId 在「第一个带 session_id 的 system 事件」就回报;
|
|
20
|
-
* - `assistant` → `message.content[]` 里 `type === 'text'` 的块拼成正文;`tool_use` /
|
|
21
|
-
* `thinking` 块**绝不**当作正文;带 `parent_tool_use_id` 的是子代理消息,也不计入;
|
|
22
|
-
* - `result` → 唯一权威收尾:`subtype === 'success' && is_error !== true` 才算成功;
|
|
23
|
-
* `result` 字段是最终答复文本(覆盖累积值),`usage` 是整轮用量。
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { normalizeUsage, requireEnum, requireEffort, requireId, requirePrompt, requireText, sanitizeError } from './jsonl.js'
|
|
27
|
-
|
|
28
|
-
/** `claude --permission-mode` 的实测取值(`claude --help`)。 */
|
|
29
|
-
const PERMISSION_MODES = ['acceptEdits', 'auto', 'bypassPermissions', 'manual', 'dontAsk', 'plan']
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 构造一轮 `claude --print` 调用。
|
|
33
|
-
*
|
|
34
|
-
* @param {object} spec
|
|
35
|
-
* @param {string} [spec.executable='claude'] - 可执行名或绝对路径。
|
|
36
|
-
* @param {string} [spec.cwd] - 接受但忽略:claude 无 cwd 选项,请用 spawn 的 cwd。
|
|
37
|
-
* @param {string} [spec.model] - `--model`,别名(sonnet/opus…)或全名。
|
|
38
|
-
* @param {string} [spec.effort] - `--effort`,实测取值 low/medium/high/xhigh/max。
|
|
39
|
-
* @param {string} [spec.profile] - 接受但忽略:codex 专属概念。
|
|
40
|
-
* @param {string} [spec.permissionMode] - `--permission-mode`。
|
|
41
|
-
* @param {string} [spec.remoteId] - 已有 session id 时走 `--resume <id>`。
|
|
42
|
-
* @param {string} spec.prompt - 正文,经 stdin 送入。
|
|
43
|
-
* @returns {{argv: string[], input: string}}
|
|
44
|
-
*/
|
|
45
|
-
export function buildCommand({ executable = 'claude', model, effort, permissionMode, remoteId, prompt } = {}) {
|
|
46
|
-
const argv = [requireText(executable, 'executable'), '--print', '--output-format', 'stream-json', '--verbose']
|
|
47
|
-
if (model !== undefined) argv.push('--model', requireText(model, 'model'))
|
|
48
|
-
if (effort !== undefined) argv.push('--effort', requireEffort(effort))
|
|
49
|
-
if (permissionMode !== undefined) argv.push('--permission-mode', requireEnum(permissionMode, PERMISSION_MODES, 'permissionMode'))
|
|
50
|
-
// 无人值守:任何权限提问都自动拒绝,而不是挂起等一个永远不会来的回答。
|
|
51
|
-
argv.push('--permission-prompts', 'none')
|
|
52
|
-
if (remoteId !== undefined) argv.push('--resume', requireId(remoteId, 'remoteId'))
|
|
53
|
-
// 正文走 stdin(`--print` 无位置参数时读 stdin),argv 里不出现 prompt。
|
|
54
|
-
return { argv, input: requirePrompt(prompt) }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 创建一个有状态的 claude 事件解析器(一轮子进程一个)。
|
|
59
|
-
*
|
|
60
|
-
* @returns {{push: (event: unknown) => ({remoteId?: string, text?: string, usage?: object, completed?: boolean, error?: string}|undefined), finish: () => {text: string, remoteId?: string, usage?: object}}}
|
|
61
|
-
*/
|
|
62
|
-
export function createParser() {
|
|
63
|
-
let remoteId
|
|
64
|
-
let text = ''
|
|
65
|
-
let usage
|
|
66
|
-
let completed = false
|
|
67
|
-
let failure
|
|
68
|
-
|
|
69
|
-
/** 失败时优先用 provider 给的错误正文,否则退化成 subtype/api_error_status。 */
|
|
70
|
-
function describeResultFailure(event) {
|
|
71
|
-
const label = typeof event.subtype === 'string' && event.subtype !== '' ? event.subtype : 'error'
|
|
72
|
-
const detail = typeof event.result === 'string' && event.result !== '' ? event.result
|
|
73
|
-
: Array.isArray(event.errors) ? event.errors.filter((item) => typeof item === 'string').slice(0, 3).join('; ') : ''
|
|
74
|
-
const status = event.api_error_status === undefined ? '' : ` (api_error_status=${event.api_error_status})`
|
|
75
|
-
return sanitizeError(detail === '' ? `claude result ${label}${status}` : `claude result ${label}${status}: ${detail}`)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
/**
|
|
80
|
-
* 投喂一个已解析的事件。
|
|
81
|
-
* @param {unknown} event - `readJsonLines` 产出的单个事件。
|
|
82
|
-
* @returns {{remoteId?: string, text?: string, usage?: object, completed?: boolean, error?: string}|undefined}
|
|
83
|
-
* 本次事件产生的动作;无动作返回 undefined。
|
|
84
|
-
* `error` 是**信息性**的:只有 `finish()` 抛错才代表这一轮失败。
|
|
85
|
-
*/
|
|
86
|
-
push(event) {
|
|
87
|
-
if (event === null || typeof event !== 'object') return undefined
|
|
88
|
-
switch (event.type) {
|
|
89
|
-
case 'system': {
|
|
90
|
-
if (typeof event.session_id !== 'string' || event.session_id === '') return undefined
|
|
91
|
-
// 只回报一次;实测 hook_started 早于 init,因此这里通常是最早的时机。
|
|
92
|
-
if (remoteId !== undefined) return undefined
|
|
93
|
-
remoteId = event.session_id
|
|
94
|
-
return { remoteId }
|
|
95
|
-
}
|
|
96
|
-
case 'assistant': {
|
|
97
|
-
// 子代理(Task 工具)的消息带 parent_tool_use_id,不能混进主答复。
|
|
98
|
-
if (typeof event.parent_tool_use_id === 'string' && event.parent_tool_use_id !== '') return undefined
|
|
99
|
-
const message = event.message
|
|
100
|
-
if (message === null || typeof message !== 'object' || !Array.isArray(message.content)) return undefined
|
|
101
|
-
const chunk = message.content
|
|
102
|
-
.filter((block) => block !== null && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string')
|
|
103
|
-
.map((block) => block.text)
|
|
104
|
-
.join('')
|
|
105
|
-
if (chunk === '') return undefined
|
|
106
|
-
// 覆盖式:一条 assistant 事件就是一条完整消息,最后一条才是最终答复。
|
|
107
|
-
text = chunk
|
|
108
|
-
return { text }
|
|
109
|
-
}
|
|
110
|
-
case 'result': {
|
|
111
|
-
const mapped = mapClaudeUsage(event.usage)
|
|
112
|
-
if (mapped !== undefined) usage = mapped
|
|
113
|
-
const succeeded = event.subtype === 'success' && event.is_error !== true
|
|
114
|
-
if (succeeded) {
|
|
115
|
-
completed = true
|
|
116
|
-
if (typeof event.result === 'string' && event.result !== '') text = event.result
|
|
117
|
-
} else {
|
|
118
|
-
failure ??= describeResultFailure(event)
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
...(mapped === undefined ? {} : { usage: mapped }),
|
|
122
|
-
...(succeeded ? { completed: true } : { error: failure }),
|
|
123
|
-
...(succeeded && typeof event.result === 'string' && event.result !== '' ? { text: event.result } : {}),
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
default:
|
|
127
|
-
// user(工具结果)/ stream_event / 未来新增事件:无语义动作。
|
|
128
|
-
return undefined
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 收流。失败或缺少 `result` 都抛错——「流结束了」不等于「回合成功了」。
|
|
134
|
-
* 幂等:可重复调用,不改变状态。
|
|
135
|
-
* @returns {{text: string, remoteId?: string, usage?: object}}
|
|
136
|
-
*/
|
|
137
|
-
finish() {
|
|
138
|
-
if (failure !== undefined) throw new Error(`claude 回合失败:${failure}`)
|
|
139
|
-
if (!completed) throw new Error('claude 流在收到成功的 result 事件之前结束')
|
|
140
|
-
return {
|
|
141
|
-
text,
|
|
142
|
-
...(remoteId === undefined ? {} : { remoteId }),
|
|
143
|
-
...(usage === undefined ? {} : { usage }),
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* 把 claude 的整轮用量映射成 DSH TokenUsage。
|
|
151
|
-
*
|
|
152
|
-
* Anthropic 的 `input_tokens` 本来就**不含**缓存(另给 `cache_read_input_tokens` /
|
|
153
|
-
* `cache_creation_input_tokens`),与 DSH 约定一致,无需扣减;
|
|
154
|
-
* `output_tokens_details.thinking_tokens` 映射为 reasoning。
|
|
155
|
-
*/
|
|
156
|
-
function mapClaudeUsage(raw) {
|
|
157
|
-
if (raw === null || typeof raw !== 'object') return undefined
|
|
158
|
-
const details = raw.output_tokens_details
|
|
159
|
-
return normalizeUsage({
|
|
160
|
-
inputTokens: raw.input_tokens,
|
|
161
|
-
outputTokens: raw.output_tokens,
|
|
162
|
-
cacheReadTokens: raw.cache_read_input_tokens,
|
|
163
|
-
cacheWriteTokens: raw.cache_creation_input_tokens,
|
|
164
|
-
reasoningTokens: details !== null && typeof details === 'object' ? details.thinking_tokens : undefined,
|
|
165
|
-
})
|
|
166
|
-
}
|