@menghuan1918/dsh-apollo 0.0.0-stage → 0.4.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 +674 -0
- package/README.md +253 -2
- package/cordis.patch.yml +78 -0
- package/docs/DEVELOPMENT.md +682 -0
- package/package.json +76 -4
- package/presets/eng/NOTICE +3 -0
- package/presets/eng/plugins/exit-guard/index.js +111 -0
- package/presets/eng/plugins/exit-guard/package.json +7 -0
- package/presets/eng/plugins/mode-instructions/index.js +124 -0
- package/presets/eng/plugins/mode-instructions/package.json +7 -0
- package/presets/eng/plugins/wait-subagent/index.js +233 -0
- package/presets/eng/plugins/wait-subagent/package.json +7 -0
- package/presets/eng-ptc.patch.yml +186 -0
- package/presets/eng.patch.yml +338 -0
- package/skills/brainstorming/SKILL.md +90 -0
- package/skills/brainstorming/spec-document-reviewer-prompt.md +50 -0
- package/skills/code-review/SKILL.md +90 -0
- package/skills/dev/SKILL.md +41 -0
- package/skills/diagnosing-bugs/SKILL.md +132 -0
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +55 -0
- package/skills/exec/SKILL.md +78 -0
- package/skills/handoff/SKILL.md +23 -0
- package/skills/main-agent-driven-development/SKILL.md +64 -0
- package/skills/tdd/SKILL.md +34 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/tests.md +77 -0
- package/src/cli/claude.js +166 -0
- package/src/cli/codex.js +164 -0
- package/src/cli/jsonl.js +216 -0
- package/src/cli-adapter.js +198 -0
- package/src/cli-sessions.js +69 -0
- package/src/client.js +1128 -0
- package/src/config.js +327 -0
- package/src/dsh-home.js +38 -0
- package/src/index.js +181 -0
- package/src/panel.js +844 -0
- package/src/settings.js +191 -0
- package/src/subagents.js +198 -0
- package/third-party/LICENSE-MIT-dsh-standard-preset +21 -0
- package/third-party/NOTICE-mattpocock-skills +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@menghuan1918/dsh-apollo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "DSH plugin: 流程技能、工程模式、Codex/Claude 可续接子代理与统一设置。纯 ESM,无构建步骤。",
|
|
5
|
+
"license": "GPL-3.0-or-later",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Menghuan1918/dsh-apollo.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "src/index.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.js",
|
|
14
|
+
"./client": "./src/client.js",
|
|
15
|
+
"./presets/*": "./presets/*",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
20
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test",
|
|
24
|
+
"prompts": "node scripts/render-prompts.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"docs",
|
|
29
|
+
"skills",
|
|
30
|
+
"presets",
|
|
31
|
+
"third-party",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"keywords": [
|
|
37
|
+
"dsh",
|
|
38
|
+
"dsh-plugin",
|
|
39
|
+
"deepseek-harness",
|
|
40
|
+
"deepseek-harness-plugin",
|
|
41
|
+
"workflow",
|
|
42
|
+
"orchestration",
|
|
43
|
+
"agent-preset"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"registry": "https://registry.npmjs.org/",
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"js-yaml": "^4.2.0",
|
|
51
|
+
"react": "18.3.1",
|
|
52
|
+
"react-test-renderer": "18.3.1"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@deepseek-ai/schemastery": "~3.18.4",
|
|
56
|
+
"dsh-better-goal": "0.1.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"react": "^18.2.0"
|
|
60
|
+
},
|
|
61
|
+
"dsh": {
|
|
62
|
+
"bundle": {
|
|
63
|
+
"patch": [
|
|
64
|
+
"./cordis.patch.yml",
|
|
65
|
+
"./presets/eng.patch.yml",
|
|
66
|
+
"./presets/eng-ptc.patch.yml"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"client": {
|
|
70
|
+
"inject": [
|
|
71
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
72
|
+
"@deepseek-ai/dsh-client-connection",
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
74
|
+
],
|
|
75
|
+
"platform": "web"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `eng-exit-guard` — 工程模式退出守卫(预设本地插件)。
|
|
3
|
+
*
|
|
4
|
+
* pi-atlas task guard 的 DSH 等价物:回合即将结束时(`agent/turn-stopping`,
|
|
5
|
+
* serial 事件——监听器跑完后 agent-loop 会重查 inbox,`agent.steer()` 加入的
|
|
6
|
+
* next-step 消息使回合不关闭),若本代理仍有运行中的直接子代理或后台任务,
|
|
7
|
+
* 注入一条提醒强制回合继续,直到 wait_subagent / job_output 等待收取、
|
|
8
|
+
* job_kill 停止,或用户中断(被中断的回合在 dispatch 前即 throwIfAborted,
|
|
9
|
+
* 天然不触发本守卫——Esc 始终是逃生口)。
|
|
10
|
+
*
|
|
11
|
+
* 无豁免机制(曾有 exitGuardIgnore 子串清单,因会被用来静默绕过守卫而移除
|
|
12
|
+
* ——实测有会话中 agent 被拦一次后自行写入豁免,后续回合全部静默放行):
|
|
13
|
+
* 任何存活的后台 job 都必须被收取或停止,回合才能收尾。需要跨回合常驻的
|
|
14
|
+
* 服务(dev server、watcher)用 pm2/systemd/裸 nohup 等脱离 DSH job 注册表
|
|
15
|
+
* 的方式管理——守卫只看本地 job 注册表与直接子代理。
|
|
16
|
+
*
|
|
17
|
+
* 每次事件读取当前 profile 的 Apollo Config.exitGuard(默认关闭)。
|
|
18
|
+
* 设置保存后即时生效;旧配置只由宿主执行一次性导入。
|
|
19
|
+
* 查询失败(listChildren / jobs.list 抛错)按 fail-open 处理:告警并放行本次
|
|
20
|
+
* 回合结束,避免查询故障把回合卡死。
|
|
21
|
+
*
|
|
22
|
+
* 零 bare import(同 wait-subagent 约束);消息构造参照 goal-round-driver /
|
|
23
|
+
* repeat-tool-reminder 的 plain-object 模式。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { currentConfig } from '../../../../src/config.js'
|
|
27
|
+
|
|
28
|
+
export const name = 'eng-exit-guard'
|
|
29
|
+
|
|
30
|
+
export const inject = ['subagents', 'agents']
|
|
31
|
+
|
|
32
|
+
/** 后台任务终态(dsh-jobs-local isTerminal 同款)。 */
|
|
33
|
+
const JOB_TERMINAL = new Set(['completed', 'killed', 'failed'])
|
|
34
|
+
|
|
35
|
+
/** 渲染一条运行项清单行。 */
|
|
36
|
+
function lineOf(prefix, id, detail) {
|
|
37
|
+
return ` - ${prefix} ${id}${detail === undefined || detail === '' ? '' : ` (${detail})`}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 构造 steer 提醒消息(模型可见,进入会话日志)。
|
|
42
|
+
* @param children - 运行中的直接子代理 listChildren 条目。
|
|
43
|
+
* @param jobs - 运行中的后台任务快照。
|
|
44
|
+
* @returns 一条 user 角色的 plugin notice 消息。
|
|
45
|
+
*/
|
|
46
|
+
function buildMessage(children, jobs) {
|
|
47
|
+
const lines = []
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
// 0.1.1-rc.2 的条目是 meta:{mode,label},0.1.2 起 mode/label 升为顶级;
|
|
50
|
+
// 兼容回退只影响展示措辞,绝不抛错。
|
|
51
|
+
lines.push(lineOf('subagent', child.id, child.label ?? child.meta?.label))
|
|
52
|
+
}
|
|
53
|
+
for (const job of jobs) {
|
|
54
|
+
lines.push(lineOf('background job', job.id, job.label === undefined ? job.kind : `${job.label}, ${job.kind}`))
|
|
55
|
+
}
|
|
56
|
+
const text = [
|
|
57
|
+
'This background work is still running, so the turn cannot end yet:',
|
|
58
|
+
...lines,
|
|
59
|
+
'',
|
|
60
|
+
'Wait for running subagents with wait_subagent (use send_message to continue one), '
|
|
61
|
+
+ 'collect background command results with job_output, and stop work you no longer need with job_kill. Finish only after all of it is handled.',
|
|
62
|
+
].join('\n')
|
|
63
|
+
return {
|
|
64
|
+
role: 'user',
|
|
65
|
+
content: [{ type: 'text', text }],
|
|
66
|
+
source: {
|
|
67
|
+
kind: 'plugin',
|
|
68
|
+
plugin: 'eng-exit-guard',
|
|
69
|
+
form: 'notice',
|
|
70
|
+
summary: `运行中的后台工作阻止回合结束(子代理 ${children.length},后台任务 ${jobs.length})`,
|
|
71
|
+
},
|
|
72
|
+
id: crypto.randomUUID(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 收集本代理仍在运行的后台工作。
|
|
78
|
+
* @param ctx - 预设插件上下文(subagents / agents 注入,jobs 可选)。
|
|
79
|
+
* @param agent - 回合即将结束的代理。
|
|
80
|
+
* @returns `{ children, jobs }`;查询失败抛出由调用方按 fail-open 处理。
|
|
81
|
+
*/
|
|
82
|
+
async function collectRunning(ctx, agent) {
|
|
83
|
+
const children = []
|
|
84
|
+
for (const entry of await ctx.subagents.listChildren(agent.id)) {
|
|
85
|
+
if (ctx.agents.get(entry.id)?.status === 'running') children.push(entry)
|
|
86
|
+
}
|
|
87
|
+
const jobs = ctx.get('jobs')
|
|
88
|
+
const runningJobs = jobs === undefined
|
|
89
|
+
? []
|
|
90
|
+
// 0.1.7 起 JobRegistry.list 的参数是 SessionId(0.1.6 及以前是 Agent 对象):
|
|
91
|
+
// 传 Agent 对象不报错但恒不匹配,守卫会静默看不见任何 owned 后台任务。
|
|
92
|
+
: jobs.list(agent.id).filter((job) => !JOB_TERMINAL.has(job.status))
|
|
93
|
+
return { children, jobs: runningJobs }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function apply(ctx) {
|
|
97
|
+
ctx.on('agent/turn-stopping', async (payload) => {
|
|
98
|
+
const agent = payload?.agent
|
|
99
|
+
if (agent === undefined || payload?.signal?.aborted) return
|
|
100
|
+
try {
|
|
101
|
+
if (!currentConfig(ctx).exitGuard) return
|
|
102
|
+
const { children, jobs } = await collectRunning(ctx, agent)
|
|
103
|
+
if (children.length === 0 && jobs.length === 0) return
|
|
104
|
+
if (payload.signal?.aborted) return
|
|
105
|
+
agent.steer(buildMessage(children, jobs))
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// fail-open:查询或注入失败时放行本次回合结束,守卫不应卡死会话。
|
|
108
|
+
ctx.logger?.warn?.(`eng-exit-guard: guard skipped: ${error instanceof Error ? error.message : String(error)}`)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `eng-mode-instructions` — preset-scoped prompt section for 工程模式
|
|
3
|
+
* (`eng`) and 工程(PTC)模式 (`eng-ptc`).
|
|
4
|
+
*
|
|
5
|
+
* Carries what no other surface states: the general engineering discipline
|
|
6
|
+
* (moved out of the user-global ~/.dsh/AGENTS.md, which keeps only
|
|
7
|
+
* machine-local conventions), the delegation policy, the subagent model tiers
|
|
8
|
+
* rendered from Apollo settings (`tiers.*`), and the exit guard contract while
|
|
9
|
+
* that switch is on. Tool mechanics, background-job etiquette and run_code
|
|
10
|
+
* syntax live in the harness prompt and tool descriptions and are NOT
|
|
11
|
+
* repeated here.
|
|
12
|
+
*
|
|
13
|
+
* The text is a function re-rendered per request from the live Apollo config,
|
|
14
|
+
* so settings edits apply without remounting; an edit costs the prefix cache
|
|
15
|
+
* once, which is acceptable for rarely changed settings. The row config
|
|
16
|
+
* `ptc: true` (set by eng-ptc) adds a rule for fanning out subagents inside
|
|
17
|
+
* one run_code program (workflow usage is left to the harness tool guidance); run_code mechanics themselves
|
|
18
|
+
* come from the harness `tools:sdk` / `tools:ptc-only` sections.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import z from '@deepseek-ai/schemastery'
|
|
22
|
+
import { currentConfig } from '../../../../src/config.js'
|
|
23
|
+
|
|
24
|
+
export const name = 'eng-mode-instructions'
|
|
25
|
+
|
|
26
|
+
export const inject = ['systemPrompt']
|
|
27
|
+
|
|
28
|
+
export const Config = z.object({
|
|
29
|
+
/** Render the PTC variant (eng-ptc). */
|
|
30
|
+
ptc: z.boolean().default(false),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const PRINCIPLES = `# Engineering mode
|
|
34
|
+
|
|
35
|
+
For coding and infrastructure work. These are defaults; explicit user instructions take precedence.
|
|
36
|
+
|
|
37
|
+
## Principles
|
|
38
|
+
- KISS / YAGNI: use the simplest approach that works. Do not add abstractions, indirection, or options the task does not need.
|
|
39
|
+
- Inspect first: read the actual files, run the actual commands, check the actual state, then judge. When evidence and judgment disagree, evidence wins. Look up unfamiliar library/API behavior instead of relying on memory.
|
|
40
|
+
- Fix the root cause: trace a bad value back to the violated assumption; no workarounds that only hide the symptom.
|
|
41
|
+
- One coherent change at a time, each with an observable result and a pass/fail check; do not bundle unrelated edits.
|
|
42
|
+
- Follow existing conventions: match surrounding naming, style, error handling, and patterns; use the project's own build/test/lint commands; read the repo's coding-standards files (if any) before writing code.
|
|
43
|
+
- Think about requirements, approach, and verification; do not draft whole implementations in your reasoning. If your reasoning starts repeating, stop, change angle, or go gather evidence.
|
|
44
|
+
|
|
45
|
+
## Avoid over-engineering and over-defending
|
|
46
|
+
- Do not add complexity for speculative needs or vanishingly unlikely race/security edge cases. If one seems necessary, first explain the concrete failure mode, its likelihood, and the complexity cost to the user and get agreement; never silently expand scope for theoretical robustness.
|
|
47
|
+
- Validate unknown input once, at the boundary that owns it, and pass settled values inward; do not re-check what a schema, constructor, or type already guarantees.
|
|
48
|
+
- Put try/catch only where the error can actually be handled; never use it to swallow errors or hide failures.
|
|
49
|
+
- Do not extract single-use helpers unless they name a real concept or isolate a genuinely complex boundary.
|
|
50
|
+
- Comment non-obvious constraints and surprising behavior, not what the code already says.
|
|
51
|
+
- Test the real implementation; minimize mocks; do not copy implementation logic into tests.
|
|
52
|
+
|
|
53
|
+
## Verification and reporting
|
|
54
|
+
- Claim success only with fresh evidence from the changed state. Passing lint does not prove runtime behavior; unit tests do not prove deployment integration.
|
|
55
|
+
- In reports, separate verified, executed-but-unverified, and not executed. Never fake progress.
|
|
56
|
+
|
|
57
|
+
## Boundaries
|
|
58
|
+
- Changing shared branches such as main or dev needs explicit authorization. Within the task, commits and merges on isolated branches are fine; create worktrees for isolation when useful.
|
|
59
|
+
- Preserve unrelated changes and keep the main worktree clean; recurring local files can go in .git/info/exclude.
|
|
60
|
+
- Commit with the configured global Git identity, never placeholder identities such as bot or agent.
|
|
61
|
+
- Tell the user when you install system packages or download dependencies outside the package manager; ask where to install missing unmanaged dependencies; prefer prebuilt packages over compiling.
|
|
62
|
+
- No filesystem-wide or whole-home-directory searches without asking first.
|
|
63
|
+
- Deviating from these rules needs the user's consent. Before asking, explore the code, tests, and prior decisions yourself; ask only questions whose answers change what you do, each with a recommended answer and why.
|
|
64
|
+
|
|
65
|
+
## Commands and processes
|
|
66
|
+
- One background job per goal: check job_list first; do not run near-duplicate commands in parallel.
|
|
67
|
+
- Work on data where it already is (including remote mounts) instead of downloading or copying large files.
|
|
68
|
+
- Before killing a process, confirm its full command line and PID with \`pgrep -af <keyword>\`, then \`kill <PID>\`; never pkill / killall.
|
|
69
|
+
- Never sleep longer than 60s at once; for longer waits use stepped probes, sentinel files, or completion notices.
|
|
70
|
+
- Run services that must outlive the turn (dev servers, watchers) under pm2/nohup outside the job registry, and say which ones are still running when you finish.`
|
|
71
|
+
|
|
72
|
+
const DELEGATION_HEAD = `## Delegation
|
|
73
|
+
- Subagents cannot see this conversation: state the goal, write scope, deliverable, and verification, and give real file paths for prior outputs.
|
|
74
|
+
- Opening reconnaissance: when starting non-trivial work in an unfamiliar or large codebase, immediately dispatch fast-tier scouts in parallel (structure, relevant modules, conventions, test entry points), all in one message; the main agent reads only the instruction files that define the task. A scout prompt needs only goal, boundaries, and deliverable.
|
|
75
|
+
- Delegate read-only, parallelizable, stateless investigation by default instead of trying it in the main thread first; when a new unknown appears mid-task and 1–2 rounds of digging do not converge, delegate it.
|
|
76
|
+
- Implementation chunks with non-overlapping write scopes can run in parallel; keep linear work, tightly coupled changes, and debugging that needs live state in the main thread.`
|
|
77
|
+
|
|
78
|
+
const FANOUT_PTC = '- For repeated same-shaped work, dispatch all subagents inside one run_code program, then collect them with wait_subagent.'
|
|
79
|
+
|
|
80
|
+
const DELEGATION_TAIL = `- When you need a subagent's result, wait patiently with wait_subagent. Do not poll or prod it with send_message; send a message only to give it genuinely new information or a correction.
|
|
81
|
+
- The main agent owns integration, review, and final verification. Subagent reports are leads, not evidence: check the actual changes yourself.
|
|
82
|
+
- When a subagent ends in an error or empty result, read why, fix the cause, and re-dispatch once; take over in the main thread only if it fails again without an external blocker, and say why.`
|
|
83
|
+
|
|
84
|
+
const EXIT_GUARD = `## Exit guard
|
|
85
|
+
- If subagents or background commands are still running when the turn would end, the system injects a reminder and the turn continues. Collect them with wait_subagent / job_output, stop unneeded ones with job_kill, then finish.`
|
|
86
|
+
|
|
87
|
+
/** One tier as model-facing argument text; an unset tier inherits the parent route. */
|
|
88
|
+
export function renderTier(tier) {
|
|
89
|
+
if (!tier?.provider || !tier?.model) return 'omit provider / model / reasoning_effort to inherit the parent route'
|
|
90
|
+
const parts = [`provider: \`${tier.provider}\``, `model: \`${tier.model}\``]
|
|
91
|
+
if (tier.reasoningEffort) parts.push(`reasoning_effort: \`${tier.reasoningEffort}\``)
|
|
92
|
+
return parts.join(' · ')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Render the full section text.
|
|
97
|
+
* @param config - normalized Apollo config (`exitGuard`, `tiers`).
|
|
98
|
+
* @param options - `{ ptc }` from this row's config.
|
|
99
|
+
* @returns the section markdown.
|
|
100
|
+
*/
|
|
101
|
+
export function renderInstructions(config, options = {}) {
|
|
102
|
+
const tiers = `## Subagent model tiers
|
|
103
|
+
- Fast tier (exploration, reconnaissance, mechanical analysis, batch triage): ${renderTier(config.tiers?.fast)}
|
|
104
|
+
- Quality tier (complex implementation, debugging, review, design): ${renderTier(config.tiers?.quality)}
|
|
105
|
+
- Start exploration on the fast tier; when coverage falls short or key questions remain, re-dispatch the unresolved part on the quality tier. Escalating means re-dispatching, not modifying an existing subagent.
|
|
106
|
+
- subagent_fork takes no model choice: it keeps the parent route to reuse the prefix cache.`
|
|
107
|
+
const blocks = [
|
|
108
|
+
PRINCIPLES,
|
|
109
|
+
[DELEGATION_HEAD, ...(options.ptc ? [FANOUT_PTC] : []), DELEGATION_TAIL].join('\n'),
|
|
110
|
+
tiers,
|
|
111
|
+
]
|
|
112
|
+
if (config.exitGuard) blocks.push(EXIT_GUARD)
|
|
113
|
+
return blocks.join('\n\n')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function apply(ctx, config) {
|
|
117
|
+
const options = { ptc: config?.ptc === true }
|
|
118
|
+
ctx.effect(() => ctx.systemPrompt.section({
|
|
119
|
+
name: 'eng:mode-instructions',
|
|
120
|
+
order: 1,
|
|
121
|
+
interpolate: false,
|
|
122
|
+
text: () => renderInstructions(currentConfig(ctx), options),
|
|
123
|
+
}), 'eng-mode-instructions.section()')
|
|
124
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
}
|