dsh-root-planner-preset 1.1.4 → 1.1.6
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 +36 -2
- package/README.zh.md +21 -2
- package/cordis.patch.yml +12 -11
- package/package.json +1 -1
- package/persona.txt +12 -11
- package/release.mjs +20 -0
- package/tools/check-pwsh-background.mjs +91 -0
- package/tools/child-status.mjs +102 -0
- package/tools/count-tool-calls.mjs +78 -0
- package/tools/extract-offered-tools.mjs +69 -0
package/README.md
CHANGED
|
@@ -21,10 +21,26 @@ This preset takes the middle line: **workers with tools, bounded by their task.*
|
|
|
21
21
|
| Nobody runs an unbounded service | Persona rules for the root and for subagents | Prompt-level, covering what a schema cannot (`ops_bash` has no job registry) |
|
|
22
22
|
| A subagent stays inside the task it was given | Persona rules: named files only, no drive-by fixes, no racing siblings | Prompt-level |
|
|
23
23
|
| Delegation depth is the user's setting | The runtime depth cap (`subagent.maxDepth`) | **Structural**: past the cap it throws `SubagentDepthError` |
|
|
24
|
+
| Live children are capped as well | `subagent.maxActiveSubagents` | **Structural**: past the cap a delegation fails with `subagent limit reached`, and it does not queue |
|
|
24
25
|
| The root decides; workers execute | Persona rules | Prompt-level |
|
|
25
26
|
|
|
26
27
|
The mask is an **allow** list: a tool added by a future plugin is invisible to subagents until it is added here.
|
|
27
28
|
|
|
29
|
+
### Two limits you will meet on a broad task
|
|
30
|
+
|
|
31
|
+
Both are Host settings, both are structural, and both are documented because they read
|
|
32
|
+
like defects the first time:
|
|
33
|
+
|
|
34
|
+
- **`subagent limit reached (active child limit: N)`** — a delegated child is
|
|
35
|
+
*resident*, not merely running: a slot returns only after its turn ends, its inbox
|
|
36
|
+
is empty, and its own children are disposed. A wide fan-out therefore holds every
|
|
37
|
+
slot until that work actually finishes, and the batch does not queue behind the
|
|
38
|
+
cap. Raise `maxActiveSubagents` (**Settings → Subagents**) if the machine can
|
|
39
|
+
carry more live agents, or dispatch a batch you can finish and collect it before
|
|
40
|
+
opening the next. The author's own profile carries 16 for this reason.
|
|
41
|
+
- **`subagent depth N exceeds maxDepth M`** — a delegation from a leaf. That is the
|
|
42
|
+
cap working, not a failure; the child reads the material itself.
|
|
43
|
+
|
|
28
44
|
## How it works
|
|
29
45
|
|
|
30
46
|
```
|
|
@@ -106,9 +122,27 @@ Then **start a new session** and pick **Root Planner** in the preset selector. F
|
|
|
106
122
|
|
|
107
123
|
**Verified**: the patch parses as loader YAML; the parsed persona is **byte-identical** to `persona.txt`; both delegation masks exclude every mutation entry point and name only tools the runtime reports as global; a clean checkout passes verification (`.gitattributes` pins LF, without which a Windows clone breaks the byte-identity check); the depth override reached the composed profile.
|
|
108
124
|
|
|
109
|
-
**
|
|
125
|
+
**Verified end to end on a real task** — a 24-descendant run over a large reverse-engineering directory tree. Every line below was read from the session logs' own persisted tool schemas, which are the registry's output rather than a model's recollection:
|
|
110
126
|
|
|
111
|
-
|
|
127
|
+
| Property | Evidence |
|
|
128
|
+
|---|---|
|
|
129
|
+
| The mask reaches every child | The root is offered **31** tools; depth-1 and depth-2 agents are offered **25**. The difference is exactly the six the mask omits: `workflow`, `present`, `job_output`, `job_list`, `job_kill`, `exit_plan_mode`. |
|
|
130
|
+
| Subagents really are workers | `write`, `edit`, `ops_replace` and `pwsh` are present at every depth and were used: 10 `write` and 60 `edit` from the root, 14 `write` from one depth-1 agent, 20 `write` from a depth-2 agent. |
|
|
131
|
+
| No subagent can leave a process running | `run_in_background` is absent from the `pwsh` schema at all three depths. (A naive log search says otherwise — the phrase also occurs in `ops_bash`'s own description, which is why `tools/check-pwsh-background.mjs` inspects the schema window instead.) |
|
|
132
|
+
| Recursion works | 12 direct children, 4 of which delegated onward to 12 grandchildren; the root itself made 14 `subagent` calls. |
|
|
133
|
+
| The depth cap holds | The tree stops at depth 2; no depth-3 session exists anywhere in the log directory. |
|
|
134
|
+
| The live-child cap is soft, not fatal | One `subagent limit reached (active child limit: 8)` during an 8-wide fan-out. Settlements returned slots as children finished, and every later delegation succeeded. |
|
|
135
|
+
|
|
136
|
+
**Corrected after the fact**: an earlier version of this file claimed the mask made
|
|
137
|
+
children leaf agents. It does not. `subagent` is registered per composed agent rather
|
|
138
|
+
than globally, so the mask can never name it — and because a restriction filters
|
|
139
|
+
globals only, a child keeps it. Recursion is bounded by `maxDepth`, not by the mask.
|
|
140
|
+
|
|
141
|
+
**How to check it yourself**: `tools/verify-session.mjs` proves which preset each
|
|
142
|
+
session ran and at what depth, but it does not read tool schemas. Use
|
|
143
|
+
`tools/extract-offered-tools.mjs` for the per-level tool list,
|
|
144
|
+
`tools/check-pwsh-background.mjs` for the process bound, and
|
|
145
|
+
`tools/count-tool-calls.mjs` for what a level actually called.
|
|
112
146
|
|
|
113
147
|
## Contributing
|
|
114
148
|
|
package/README.zh.md
CHANGED
|
@@ -21,10 +21,18 @@
|
|
|
21
21
|
| 谁都不跑无界服务 | 根与子代理的人设规则 | 提示词级,覆盖 schema 管不到的部分(`ops_bash` 没有任务注册表) |
|
|
22
22
|
| 子代理不越出被交代的任务 | 人设规则:只动点名文件、不顺手修、不与兄弟抢文件 | 提示词级 |
|
|
23
23
|
| 委派层数由用户配置 | 运行时深度上限(`subagent.maxDepth`) | **结构性**:超过上限直接抛 `SubagentDepthError` |
|
|
24
|
+
| 在世子代理数也有上限 | `subagent.maxActiveSubagents` | **结构性**:超过上限时委派直接失败并报 `subagent limit reached`,**不会排队** |
|
|
24
25
|
| 根决策、工人执行 | 人设规则 | 提示词级 |
|
|
25
26
|
|
|
26
27
|
白名单是 **allow 而非 deny**:将来某个插件新增的工具,对子代理默认不可见。
|
|
27
28
|
|
|
29
|
+
### 大任务上你会撞到的两个上限
|
|
30
|
+
|
|
31
|
+
两个都是 Host 设置、都是结构性的,也都列在这里——因为它们第一次出现时看着像 bug:
|
|
32
|
+
|
|
33
|
+
- **`subagent limit reached (active child limit: N)`** —— 子代理是**驻留**的,不只是"在跑":只有它的轮次结束、收件箱清空、它自己的子代理也销毁之后,槽位才归还。所以宽扇出会把槽位一直占着,直到那批活真正做完;而且**超额的那批不会排队**。机器扛得住就把 `maxActiveSubagents`(**设置 → 子智能体**)调大,否则只派一批你能做完的、收完结果再开下一批。作者自己的 profile 因此设为 16。
|
|
34
|
+
- **`subagent depth N exceeds maxDepth M`** —— 叶子层发起委派。这是上限在正常工作,不是故障;子代理自己读就是了。
|
|
35
|
+
|
|
28
36
|
## 工作方式
|
|
29
37
|
|
|
30
38
|
```
|
|
@@ -94,9 +102,20 @@ dsh plugin --profile web add github:XiaoOu2024/dsh-root-planner-preset
|
|
|
94
102
|
|
|
95
103
|
**已验证**:补丁可被 loader 解析;解析出的人设与 `persona.txt` **逐字节相同**;两个委派工具的白名单排除了所有写入入口、且只包含**运行时报告的全局工具**;干净检出后校验通过(`.gitattributes` 已钉死 LF,否则 Windows 克隆会破坏逐字节校验);深度覆盖已落到合成配置。
|
|
96
104
|
|
|
97
|
-
|
|
105
|
+
**已在真实任务上端到端验证**——那次运行在一个大型逆向工程目录树上产生了 **24 个后代会话**。下面每一条都读自**会话日志自己持久化的工具 schema**,那是注册表的输出,不是模型的回忆:
|
|
106
|
+
|
|
107
|
+
| 性质 | 证据 |
|
|
108
|
+
|---|---|
|
|
109
|
+
| 白名单覆盖每一层 | 根被提供 **31** 个工具;深度 1 与深度 2 的 agent 都是 **25** 个。差额正好是白名单排除的那 6 个:`workflow`、`present`、`job_output`、`job_list`、`job_kill`、`exit_plan_mode`。 |
|
|
110
|
+
| 子代理确实是工人 | `write`、`edit`、`ops_replace`、`pwsh` 在每一层都在,而且真的被用了:根 10 次 `write` + 60 次 `edit`,某个深度 1 的子代理 14 次 `write`,某个深度 2 的孙代理 20 次 `write`。 |
|
|
111
|
+
| 子代理留不下运行中的进程 | `run_in_background` 在三个层级的 `pwsh` schema 里**都不存在**。(朴素的全日志搜索会给出相反答案——`ops_bash` 自己的描述里也有这个词,所以要用 `tools/check-pwsh-background.mjs` 看 schema 窗口。) |
|
|
112
|
+
| 递归成立 | 12 个直接子代理,其中 4 个继续派出了 12 个孙代理;根自己做了 14 次 `subagent` 调用。 |
|
|
113
|
+
| 深度上限有效 | 树停在深度 2;整个日志目录里不存在任何深度 3 的会话。 |
|
|
114
|
+
| 在世上限是软拒绝而非致命 | 8 路扇出时出现一次 `subagent limit reached (active child limit: 8)`;子代理完结时结算通知归还了槽位,此后每次委派都成功。 |
|
|
115
|
+
|
|
116
|
+
**一处事后更正**:本文件早期版本称"白名单让子代理成为叶子"。**并非如此。** `subagent` 是按每个被组合 Agent 注册、而非全局注册,所以白名单**永远无法写下它的名字**;而限制只过滤全局工具,因此子代理**保有此工具**。递归的边界是 `maxDepth`,不是白名单。
|
|
98
117
|
|
|
99
|
-
|
|
118
|
+
**想自己核**:`tools/verify-session.mjs` 能证明每个会话跑的是哪个预设、深度多少,但它不读工具 schema。逐层工具清单用 `tools/extract-offered-tools.mjs`,进程边界用 `tools/check-pwsh-background.mjs`,实际调用用 `tools/count-tool-calls.mjs`。
|
|
100
119
|
|
|
101
120
|
## 参与维护
|
|
102
121
|
|
package/cordis.patch.yml
CHANGED
|
@@ -42,17 +42,18 @@
|
|
|
42
42
|
## Root agent rules
|
|
43
43
|
|
|
44
44
|
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
45
|
-
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
46
|
-
3.
|
|
47
|
-
4.
|
|
48
|
-
5.
|
|
49
|
-
6.
|
|
50
|
-
7.
|
|
51
|
-
8.
|
|
52
|
-
9.
|
|
53
|
-
10.
|
|
54
|
-
11.
|
|
55
|
-
12.
|
|
45
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
46
|
+
3. **Mind the live capacity, not just the work.** Delegated children are resident: a slot returns only after a child's turn ends, its inbox is empty, and its own children are gone. A wide fan-out therefore holds every slot until the work actually finishes, and starting more than the environment allows fails outright with a limit error — the batch does not queue. So dispatch a batch you can finish, collect its results before opening the next, and when a delegation is refused for capacity, that is information about your scheduling rather than a reason to retry: use the agents you already have, or finish what is in flight first. Neither the depth limit nor this capacity is yours to change; plan inside them.
|
|
47
|
+
4. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
48
|
+
5. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
49
|
+
6. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 9 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
50
|
+
7. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
51
|
+
8. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
52
|
+
9. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
53
|
+
10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
54
|
+
11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
55
|
+
12. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself.
|
|
56
|
+
13. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
56
57
|
|
|
57
58
|
## Subagent rules
|
|
58
59
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-root-planner-preset",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH agent preset that outsources file reading to strictly read-only subagents, while the root agent keeps the plan and owns every file mutation.",
|
|
6
6
|
"license": "MIT",
|
package/persona.txt
CHANGED
|
@@ -21,17 +21,18 @@ Reading is not planning. Raw file contents and raw query results belong in the a
|
|
|
21
21
|
## Root agent rules
|
|
22
22
|
|
|
23
23
|
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
24
|
-
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
28
|
-
6.
|
|
29
|
-
7.
|
|
30
|
-
8.
|
|
31
|
-
9.
|
|
32
|
-
10.
|
|
33
|
-
11.
|
|
34
|
-
12.
|
|
24
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
25
|
+
3. **Mind the live capacity, not just the work.** Delegated children are resident: a slot returns only after a child's turn ends, its inbox is empty, and its own children are gone. A wide fan-out therefore holds every slot until the work actually finishes, and starting more than the environment allows fails outright with a limit error — the batch does not queue. So dispatch a batch you can finish, collect its results before opening the next, and when a delegation is refused for capacity, that is information about your scheduling rather than a reason to retry: use the agents you already have, or finish what is in flight first. Neither the depth limit nor this capacity is yours to change; plan inside them.
|
|
26
|
+
4. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
27
|
+
5. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
28
|
+
6. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 9 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
29
|
+
7. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
30
|
+
8. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
31
|
+
9. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
32
|
+
10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
33
|
+
11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
34
|
+
12. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself.
|
|
35
|
+
13. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
35
36
|
|
|
36
37
|
## Subagent rules
|
|
37
38
|
|
package/release.mjs
CHANGED
|
@@ -220,6 +220,26 @@ step(`Upgrade the local "${profile}" profile so the machine runs what was publis
|
|
|
220
220
|
if (dryRun || skipInstall) {
|
|
221
221
|
process.stdout.write(' skipped\n');
|
|
222
222
|
} else {
|
|
223
|
+
// The profile installs through the CONFIGURED registry, which is a mirror that
|
|
224
|
+
// lags the official one — so a version that just went live can still answer
|
|
225
|
+
// "No matching version found" here. That is what made this step fail after the
|
|
226
|
+
// 1.1.4 release, with nothing wrong on the npm side. Wait for the mirror, then
|
|
227
|
+
// name the official registry anyway so the install works either way.
|
|
228
|
+
const mirror = process.env.DSH_RELEASE_MIRROR ?? 'https://registry.npmmirror.com/';
|
|
229
|
+
const probeMirror = () =>
|
|
230
|
+
spawnSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', '-m', '20', '-x', PROXY, `${mirror}${manifest.name}/${version}`], {
|
|
231
|
+
encoding: 'utf8',
|
|
232
|
+
}).stdout?.trim();
|
|
233
|
+
let mirrored = false;
|
|
234
|
+
for (let attempt = 1; attempt <= 30 && !mirrored; attempt += 1) {
|
|
235
|
+
mirrored = probeMirror() === '200';
|
|
236
|
+
if (!mirrored) {
|
|
237
|
+
if (attempt === 1) process.stdout.write(` waiting for ${mirror} to mirror ${version}…\n`);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 20_000));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
process.stdout.write(mirrored ? ` ${mirror} has it\n` : ` ${mirror} still lacks it; using the official registry\n`);
|
|
242
|
+
|
|
223
243
|
// `dsh` is a shell shim on Windows too, so it is invoked through Node rather
|
|
224
244
|
// than spawned directly. DSH_RELEASE_DSH overrides the entry point.
|
|
225
245
|
const dshEntry = process.env.DSH_RELEASE_DSH ?? 'D:/nodejs/npm_global/node_modules/@deepseek-ai/dsh/lib/bin.js';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Checks whether `run_in_background` still exists inside a session's `pwsh` tool
|
|
2
|
+
// schema — the one property this preset enforces structurally rather than by asking
|
|
3
|
+
// the model nicely.
|
|
4
|
+
//
|
|
5
|
+
// A whole-log substring search cannot answer it: the phrase also appears in
|
|
6
|
+
// `ops_bash`'s own description on this machine, so the naive check reports "present"
|
|
7
|
+
// for a session where the parameter has genuinely been removed.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node tools/check-pwsh-background.mjs <session-dir> [...]
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
14
|
+
|
|
15
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
16
|
+
function frameEnd(buf) {
|
|
17
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
18
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
19
|
+
}
|
|
20
|
+
return buf.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
24
|
+
async function decompressAll(buf) {
|
|
25
|
+
let rest = buf;
|
|
26
|
+
let text = '';
|
|
27
|
+
while (rest.length > 0) {
|
|
28
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
29
|
+
try {
|
|
30
|
+
text += await new Promise((resolve, reject) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
const dec = createZstdDecompress();
|
|
33
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
34
|
+
dec.on('error', reject);
|
|
35
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
36
|
+
dec.end(slice);
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (slice.length >= rest.length) break;
|
|
42
|
+
rest = rest.subarray(slice.length);
|
|
43
|
+
}
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find every `"name":"<tool>"` occurrence and return the window of text that follows
|
|
49
|
+
* it — close enough to hold that tool's own parameters object.
|
|
50
|
+
* @param text - whole decompressed log.
|
|
51
|
+
* @param tool - tool name to locate.
|
|
52
|
+
* @returns the following windows, longest first.
|
|
53
|
+
*/
|
|
54
|
+
function windowsAfter(text, tool) {
|
|
55
|
+
const needle = `\\"name\\":\\"${tool}\\"`;
|
|
56
|
+
const plain = `"name":"${tool}"`;
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const pattern of [needle, plain]) {
|
|
59
|
+
let from = 0;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const at = text.indexOf(pattern, from);
|
|
62
|
+
if (at === -1) break;
|
|
63
|
+
out.push(text.slice(at, at + 4000));
|
|
64
|
+
from = at + pattern.length;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out.sort((a, b) => b.length - a.length);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const dir of process.argv.slice(2)) {
|
|
71
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
72
|
+
const header = JSON.parse(text.split('\n')[0]);
|
|
73
|
+
|
|
74
|
+
const pwshWindows = windowsAfter(text, 'pwsh');
|
|
75
|
+
// The parameters object is the one that mentions the tool's own argument names.
|
|
76
|
+
const schemaWindow = pwshWindows.find((w) => w.includes('workdir') || w.includes('command')) ?? pwshWindows[0] ?? '';
|
|
77
|
+
const backgroundInSchema = /run_in_background/.test(schemaWindow);
|
|
78
|
+
const backgroundAnywhere = text.includes('run_in_background');
|
|
79
|
+
|
|
80
|
+
process.stdout.write(
|
|
81
|
+
`\n${dir.split(/[\\/]/).pop().slice(0, 20)} depth=${header?.delegationDepth}\n` +
|
|
82
|
+
` "run_in_background" anywhere in the log : ${backgroundAnywhere}\n` +
|
|
83
|
+
` inside the pwsh schema window : ${backgroundInSchema}` +
|
|
84
|
+
`${backgroundInSchema ? ' <-- the parameter is still offered' : ' <-- removed, as intended'}\n` +
|
|
85
|
+
` pwsh schema window size : ${schemaWindow.length} chars\n`,
|
|
86
|
+
);
|
|
87
|
+
if (backgroundAnywhere && !backgroundInSchema) {
|
|
88
|
+
const at = text.indexOf('run_in_background');
|
|
89
|
+
process.stdout.write(` first mention context: …${text.slice(Math.max(0, at - 120), at + 60)}…\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Reports whether each child session in a run is finished or still active, from its
|
|
2
|
+
// own log — the question behind "subagent limit reached": are slots being held by
|
|
3
|
+
// children that already stopped, or by children that are genuinely still working?
|
|
4
|
+
//
|
|
5
|
+
// Usage: node tools/child-status.mjs <root-session-dir>
|
|
6
|
+
|
|
7
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
10
|
+
|
|
11
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
12
|
+
function frameEnd(buf) {
|
|
13
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
14
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
15
|
+
}
|
|
16
|
+
return buf.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
20
|
+
async function decompressAll(buf) {
|
|
21
|
+
let rest = buf;
|
|
22
|
+
let text = '';
|
|
23
|
+
while (rest.length > 0) {
|
|
24
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
25
|
+
try {
|
|
26
|
+
text += await new Promise((resolve, reject) => {
|
|
27
|
+
const chunks = [];
|
|
28
|
+
const dec = createZstdDecompress();
|
|
29
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
30
|
+
dec.on('error', reject);
|
|
31
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
32
|
+
dec.end(slice);
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (slice.length >= rest.length) break;
|
|
38
|
+
rest = rest.subarray(slice.length);
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rootDir = process.argv[2];
|
|
44
|
+
if (rootDir === undefined) {
|
|
45
|
+
process.stderr.write('usage: node tools/child-status.mjs <root-session-dir>\n');
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const base = join(rootDir, '..');
|
|
50
|
+
const rootId = rootDir.split(/[\\/]/).pop();
|
|
51
|
+
const rows = [];
|
|
52
|
+
|
|
53
|
+
for (const name of readdirSync(base)) {
|
|
54
|
+
const file = join(base, name, 'session.v4.jsonl.zstd');
|
|
55
|
+
try {
|
|
56
|
+
if (!statSync(file).isFile()) continue;
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const text = await decompressAll(readFileSync(file));
|
|
61
|
+
const events = text
|
|
62
|
+
.split('\n')
|
|
63
|
+
.filter((line) => line.trim().length > 0)
|
|
64
|
+
.flatMap((line) => {
|
|
65
|
+
try {
|
|
66
|
+
return [JSON.parse(line)];
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const header = events[0];
|
|
72
|
+
if (name !== rootId && header?.origin !== 'subagent') continue;
|
|
73
|
+
|
|
74
|
+
const types = new Map();
|
|
75
|
+
for (const event of events) types.set(event.type, (types.get(event.type) ?? 0) + 1);
|
|
76
|
+
|
|
77
|
+
const last = events[events.length - 1];
|
|
78
|
+
rows.push({
|
|
79
|
+
id: name,
|
|
80
|
+
depth: header?.delegationDepth,
|
|
81
|
+
mtime: statSync(file).mtime,
|
|
82
|
+
events: events.length,
|
|
83
|
+
lastType: last?.type,
|
|
84
|
+
lastTime: last?.time,
|
|
85
|
+
// A turn that ended and left nothing pending is the shape of a settled child.
|
|
86
|
+
turnEnds: types.get('turn/end') ?? 0,
|
|
87
|
+
hasInboxPending: types.has('agent/inbox/spliced') && !types.has('agent/inbox/claimed'),
|
|
88
|
+
cancels: types.get('agent/cancel') ?? 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rows.sort((a, b) => (a.lastTime ?? 0) - (b.lastTime ?? 0));
|
|
93
|
+
const fmt = (ms) => (ms === undefined ? '?' : new Date(ms).toISOString().slice(11, 19));
|
|
94
|
+
|
|
95
|
+
process.stdout.write(`${rows.length} agent session(s) under ${base}\n\n`);
|
|
96
|
+
process.stdout.write('depth turnEnds events last event type last event time session\n');
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
process.stdout.write(
|
|
99
|
+
`${String(row.depth ?? '-').padEnd(6)} ${String(row.turnEnds).padEnd(9)} ${String(row.events).padEnd(7)} ` +
|
|
100
|
+
`${String(row.lastType ?? '-').padEnd(25)} ${fmt(row.lastTime).padEnd(16)} ${row.id.slice(0, 20)}\n`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Counts the tool calls actually made in one session, by name.
|
|
2
|
+
//
|
|
3
|
+
// Written to settle a question that string counting cannot: which delegation tool a
|
|
4
|
+
// given level really called, and therefore which tools the child mask actually left
|
|
5
|
+
// visible. A raw occurrence count of "subagent" matches "subagent_fork" too, and the
|
|
6
|
+
// preset's own prose mentions both.
|
|
7
|
+
//
|
|
8
|
+
// Usage: node tools/count-tool-calls.mjs <session-dir> [...]
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
13
|
+
|
|
14
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
15
|
+
function frameEnd(buf) {
|
|
16
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
17
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
18
|
+
}
|
|
19
|
+
return buf.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
23
|
+
async function decompressAll(buf) {
|
|
24
|
+
let rest = buf;
|
|
25
|
+
let text = '';
|
|
26
|
+
while (rest.length > 0) {
|
|
27
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
28
|
+
try {
|
|
29
|
+
text += await new Promise((resolve, reject) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
const dec = createZstdDecompress();
|
|
32
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
33
|
+
dec.on('error', reject);
|
|
34
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
35
|
+
dec.end(slice);
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (slice.length >= rest.length) break;
|
|
41
|
+
rest = rest.subarray(slice.length);
|
|
42
|
+
}
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const dir of process.argv.slice(2)) {
|
|
47
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
48
|
+
const events = text
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter((line) => line.trim().length > 0)
|
|
51
|
+
.flatMap((line) => {
|
|
52
|
+
try {
|
|
53
|
+
return [JSON.parse(line)];
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const header = events[0];
|
|
60
|
+
const calls = new Map();
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
if (event.type !== 'tool/call') continue;
|
|
63
|
+
const name = event.data?.name ?? event.data?.message?.name;
|
|
64
|
+
if (typeof name !== 'string') continue;
|
|
65
|
+
calls.set(name, (calls.get(name) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const name = dir.split(/[\\/]/).pop().slice(0, 20);
|
|
69
|
+
process.stdout.write(
|
|
70
|
+
`\n${name} depth=${header?.delegationDepth} origin=${header?.origin} ${events.length} events\n`,
|
|
71
|
+
);
|
|
72
|
+
const rows = [...calls.entries()].sort((a, b) => b[1] - a[1]);
|
|
73
|
+
process.stdout.write(` ${rows.length} distinct tools called: ${rows.map(([t, n]) => `${t}×${n}`).join(', ') || '(none)'}\n`);
|
|
74
|
+
const delegation = rows.filter(([t]) => t.startsWith('subagent'));
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
` delegation calls: ${delegation.map(([t, n]) => `${t}×${n}`).join(', ') || '(none)'}\n`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Extracts the tool names a session was actually offered, from the tool schemas the
|
|
2
|
+
// session log persists.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: a subagent's own account of its tools is a language-model
|
|
5
|
+
// recollection, and it can be wrong in both directions (the first such report
|
|
6
|
+
// omitted `write` from its list while claiming to discuss it). The persisted
|
|
7
|
+
// schemas are the registry's own output, so they settle questions like "does the
|
|
8
|
+
// child mask really apply?" and "is run_in_background gone?" without guessing.
|
|
9
|
+
//
|
|
10
|
+
// Usage: node tools/extract-offered-tools.mjs <session-dir> [...]
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
15
|
+
|
|
16
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
17
|
+
function frameEnd(buf) {
|
|
18
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
19
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
20
|
+
}
|
|
21
|
+
return buf.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
25
|
+
async function decompressAll(buf) {
|
|
26
|
+
let rest = buf;
|
|
27
|
+
let text = '';
|
|
28
|
+
while (rest.length > 0) {
|
|
29
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
30
|
+
try {
|
|
31
|
+
text += await new Promise((resolve, reject) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const dec = createZstdDecompress();
|
|
34
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
35
|
+
dec.on('error', reject);
|
|
36
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
37
|
+
dec.end(slice);
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (slice.length >= rest.length) break;
|
|
43
|
+
rest = rest.subarray(slice.length);
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const dir of process.argv.slice(2)) {
|
|
49
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
50
|
+
|
|
51
|
+
// A tool schema is an object carrying `name` plus `parameters` (and `description`).
|
|
52
|
+
// Collect every name that appears next to a parameters object anywhere in the log.
|
|
53
|
+
const names = new Set();
|
|
54
|
+
for (const match of text.matchAll(/\{\\?"name\\?":\\?"([a-zA-Z_][a-zA-Z0-9_]*)\\?":\{[^{}]*\\?"type\\?":\\?"object\\?"/g)) {
|
|
55
|
+
names.add(match[1]);
|
|
56
|
+
}
|
|
57
|
+
for (const match of text.matchAll(/\\?"name\\?":\\?"([a-zA-Z_][a-zA-Z0-9_]*)\\?",\\?"description\\?":/g)) {
|
|
58
|
+
names.add(match[1]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const header = JSON.parse(text.split('\n')[0]);
|
|
62
|
+
const listed = [...names].sort();
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
`\n${dir.split(/[\\/]/).pop().slice(0, 20)} depth=${header?.delegationDepth}\n ${listed.length} names: ${listed.join(', ')}\n`,
|
|
65
|
+
);
|
|
66
|
+
for (const probe of ['workflow', 'present', 'job_output', 'exit_plan_mode', 'subagent', 'subagent_fork', 'pwsh', 'write']) {
|
|
67
|
+
process.stdout.write(` ${probe.padEnd(16)} ${names.has(probe) ? 'present' : 'ABSENT'}\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|