dsh-parallel-tools-prompt 0.0.0-stage → 0.2.1-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lloydzhou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,76 @@
1
- # Temporary Holding Version
1
+ # dsh-parallel-tools-prompt
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 独立 DeepSeek Harness host-only 插件:用稳定的依赖感知提示,引导模型把当前已就绪、互不依赖的必要工具调用放在同一响应里,减少不必要的模型—工具往返。
4
+
5
+ **不是 DAG 执行器,也不是费用优化保证。** 不新增模型请求、不增加工具、不更改宿主安全分类、权限或调度器。效果取决于模型遵循度、任务依赖与运行环境,尚未做付费前向 A/B。
6
+
7
+ ## 行为
8
+
9
+ - 默认只作用于创建/恢复时的 `standard` 根 Agent;跳过子智能体、PTC、minimal、creator 等非目标会话。
10
+ - 使用公开 `agent/created`、`agents.roots()`、session projection 与 `systemPrompt.section()`,不替换 preset。
11
+ - 固定 section 名称 `parallel-tools:guidance`、order 1050、禁止插值;不注入时间戳、工具列表或每轮变化的数据。
12
+ - 同一 Agent 幂等注册;插件卸载与 Agent 释放都清理 section。插件重载时为仍存活的目标根 Agent 补挂 section;源码更新建议重启宿主加载新模块。
13
+ - bundle 默认开启 `contextReminder`:通过公开 `agent.inject()` 在下一步骤边界注入一次带 `[parallel-tools:scheduling]` 标记的对话提醒,强调提前规划就绪批次以减少模型往返。直接使用插件时此选项默认关闭。提醒进入会话历史,卸载不会删除;已持久化标记用于避免重复注入。这不是每轮重复提醒,也不保证长期遵循。
14
+ - 单次调用合理时仍单次;路径、ID等参数必须已知;读后写、共享文件冲突、权限审批保持依赖顺序。不发明batch工具或额外规划轮次。
15
+ - 同轮多个工具调用不代表真正同时执行;执行并发仍由DSH调度器决定。
16
+
17
+ ## 版本、依赖与发布
18
+
19
+ 插件版本 `0.2.1-rc.1` 跟随 DSH 的 `0.2.1` 版本线,RC 序号独立递增,不等于宿主 RC 序号。最低宿主要求 `>=0.2.1-rc.3`,Node.js `>=24.9.0`。开发锁文件以 RC.5 为基线;RC.3 的相关接口已做源码比对,完整插件测试本次运行在 RC.5 依赖上,不冒充两套运行验收。
20
+
21
+ 宿主库以 peerDependencies 声明,同时列入 devDependencies 供构建测试:Cordis `4.0.5`、Schemastery `3.18.5`、DSH agent/llm/preset-registry/session-projection/system-prompt 使用 `^0.2.1-rc.3`。不将宿主服务打包为普通运行依赖,避免重复服务身份。semver caret 不跨 `0.3`,也不自动接纳不同版本号的预发布版本。
22
+
23
+ 建议使用 `npm ci` 配合 npm lock,避免交替更新两种锁文件。包发布白名单包含构建产物、bundle patch、README、许可证与分析脚本,不包含测试、原始会话或调试捕获。`prepack` 自动构建;预发布默认 `next` tag。确认版本未占用后由维护者执行 `npm publish`;已发布版本不能覆盖。
24
+
25
+ ## 开发与安装
26
+
27
+ 尚未发布 npm;已在本地 Web profile 链接安装并进行人工观察。一次探索任务出现连续多调用批次,但模型同时发生切换,不能据此归因于插件或宣称 token/费用节省。
28
+
29
+ ```sh
30
+ npm ci
31
+ npm run build
32
+ npm run typecheck
33
+ npm test
34
+ npm pack --dry-run
35
+ ```
36
+
37
+ 确认愿意启用后,手动安装本目录到目标 profile,例如 `dsh plugin --profile web add .`;Desktop 选择 `--profile desktop`。本插件没有客户端 UI,不需要 `dsh.client`。
38
+
39
+ 直接调用插件默认 `enabled: false`;bundle显式配置 `enabled: true`,安装bundle即选择启用。配置:
40
+
41
+ ```yaml
42
+ enabled: true
43
+ contextReminder: true
44
+ presetIds: [standard]
45
+ ```
46
+
47
+ **注意:profile 的 `cordis.patch.yml` 覆盖本插件时,`config` 是整体替换、不与 bundle 默认值深合并。** 覆盖项必须写全需要的字段(至少 `enabled: true`、`presetIds`),否则 `enabled` 回落为默认 `false`,插件静默不注入。验收方式:新会话的 `system/message` 中应出现 `Tool-call scheduling:`。
48
+
49
+ `presetIds` 是精确ID列表,不是正则。自定义目标仅适用于原生工具调用preset;不要把PTC加入以免对专用执行模式施加不合适的指导。根会话fork可以保留历史parentSession,不会仅因此被当作子智能体。
50
+
51
+ 卸载会释放注册;已有模型历史中的旧指令不会被改写或删除。若宿主需要重新投影系统提示,生效边界由宿主管理;稳妥方式是重启后继续原会话。首次安装可能改变system前缀并影响一次缓存复用;不能保证零缓存影响。
52
+
53
+ ## 参考与验证边界
54
+
55
+ 主要借鉴 [bash-agent #99](https://github.com/lloydzhou/bash-agent/issues/99) 的就绪批次思路及 [#100](https://github.com/lloydzhou/bash-agent/issues/100) 对实测/离线估算的讨论;同时参考 [Anthropic并行调用指南](https://platform.claude.com/docs/en/agents-and-tools/tool-use/parallel-tool-use)、[LLMCompiler](https://github.com/SqueezeAILab/LLMCompiler) 和 [Superpowers并行委派skill](https://github.com/obra/superpowers/blob/main/skills/dispatching-parallel-agents/SKILL.md)。本插件提示词为针对DSH自行编写的简短文本,不移植其执行器或照搬专用工具调用格式。
56
+
57
+ LLMCompiler收益来自框架与调度;bash-agent历史DAG合并收益是离线估算;这些都不是本插件的实测收益。生命周期测试只证明接入/清理正确,不证明模型更快、更省。
58
+
59
+ ## 临时 LLM 提示调试
60
+
61
+ 默认 `debugEnabled: false`。调试模式在公开 `llm/stream` 边界记录归一化请求中的完整系统/开发者提示,不是最终HTTP报文;后续适配器仍可能变换它。仅指定会话命中,日志不收集用户/工具结果正文、认证头、工具参数schema或endpoint。系统提示本身也可能包含内部路径和秘密,请将日志视为敏感数据,不提交仓库或上传公开issue。
62
+
63
+ 配置 `debugEnabled: true`、精确的 `debugSessionId`、绝对路径 `debugOutputDir` 和 `debugMaxCalls`(默认3,上限10)。输出目录须当前用户所有且权限0700;其中放一个私有普通文件 `enabled` 才会开始捕获。关闭配置或移走此标记后停止捕获,单进程达到次数上限也会停止;默认模式不写文件。新代码首次加载需重启宿主。调试结束须移走标记并关闭配置,避免重启重新开始捕获。
64
+
65
+ ## 评估建议
66
+
67
+ 主指标是每完成任务的模型响应/尝试数量,而不是工具调用总数。固定任务、模型、reasoning、权限、工作目录与工具;对比输出tokens、正确率、重试/失败及wall time;报告配对分布、中位数、IQR,单独标记首次提示变化的缓存成本。
68
+
69
+ 离线分析仅对显式指定的日志文件运行,不默认扫描个人会话目录,不输出内容或路径:
70
+
71
+ ```sh
72
+ node scripts/analyze-session.mjs --help
73
+ node scripts/analyze-session.mjs /path/to/session.v4.jsonl.zstd
74
+ ```
75
+
76
+ 按assistant/message中的tool-call块计算批量率和calls/response均值、中位数;`assistant/attempt`与`tool/call`另计,不当作远端计费API调用。输出tokens同时报告已知总量与未知响应数量。日志中的继承/seed事件会计入,重复输入文件不去重;不能据此推断实际并发、LLM耗时或因果收益。zstd压缩及展开数据各限64MiB。更详细口径见脚本帮助和测试。
@@ -0,0 +1,9 @@
1
+ # Installing/enabling this bundle is the explicit opt-in. Direct plugin use
2
+ # defaults to enabled:false. No preset replacement or scheduler changes.
3
+ - insert:
4
+ - id: parallel-tools-prompt
5
+ name: dsh-parallel-tools-prompt
6
+ config:
7
+ enabled: true
8
+ contextReminder: true
9
+ presetIds: [standard]
package/lib/debug.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ /** Explicit opt-in for a temporary, sensitive normalized-prompt capture. */
3
+ export interface PromptDebugOptions {
4
+ enabled?: boolean;
5
+ /** Exact identity; no wildcard, prefix, child-session, or global matching. */
6
+ sessionId: string;
7
+ /** Operator-created, owned 0700 directory containing a regular `enabled` file. */
8
+ outputDir: string;
9
+ /** Maximum enabled matching calls attempted by this installation (default 3, maximum 10). */
10
+ maxCalls?: number;
11
+ }
12
+ /**
13
+ * Install optional public llm/stream observation. Disabled by default. The
14
+ * operator must create outputDir with mode 0700 and its `enabled` sentinel;
15
+ * absence creates nothing. Removing the sentinel disables subsequent captures
16
+ * without restart (already-written captures remain sensitive). Never mkdir or
17
+ * chmod a user path. Reject a whole capture over 2 MiB, never truncate it.
18
+ *
19
+ * This is normalized input at this middleware's position, NOT the final HTTP
20
+ * request; later middleware/adapters may transform it. System/developer text
21
+ * itself can contain secrets. No user/assistant/tool text, schemas, headers,
22
+ * credentials, endpoints or standard logging are included. The untouched stream
23
+ * is returned directly; no extra model calls or stream consumption occur.
24
+ */
25
+ export declare function installPromptDebug(ctx: Context, options: PromptDebugOptions): () => void;
26
+ //# sourceMappingURL=debug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAMnD,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAuED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,IAAI,CA2CxF"}
package/lib/debug.js ADDED
@@ -0,0 +1,136 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, constants, fstatSync, lstatSync, openSync, writeFileSync } from 'node:fs';
3
+ import { isAbsolute, join, parse, resolve, sep } from 'node:path';
4
+ const MAX_BYTES = 2 * 1024 * 1024;
5
+ const PROCESS_LIMIT = 10;
6
+ // A reload or a second installation cannot reset the process-wide allowance.
7
+ const BUDGET_KEY = Symbol.for('dsh-parallel-tools-prompt.prompt-debug.budget.v1');
8
+ const budgets = globalThis;
9
+ function safeId(value) {
10
+ return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._/-]{0,159}$/.test(value)
11
+ ? value : undefined;
12
+ }
13
+ /** Reject symlinks in every component, not just the final output directory. */
14
+ function privateDirectory(path) {
15
+ let current = parse(path).root;
16
+ for (const component of path.slice(current.length).split(sep).filter(Boolean)) {
17
+ current = join(current, component);
18
+ const stat = lstatSync(current);
19
+ if (!stat.isDirectory() || stat.isSymbolicLink())
20
+ throw new Error('unsafe directory');
21
+ }
22
+ const stat = lstatSync(path);
23
+ if (process.getuid === undefined || stat.uid !== process.getuid() || (stat.mode & 0o777) !== 0o700) {
24
+ throw new Error('non-private directory');
25
+ }
26
+ return stat;
27
+ }
28
+ function enabled(path) {
29
+ const stat = lstatSync(join(path, 'enabled'));
30
+ return stat.isFile() && !stat.isSymbolicLink() && stat.uid === process.getuid?.();
31
+ }
32
+ function capture(options) {
33
+ let textBytes = 0;
34
+ const text = (value) => {
35
+ if (typeof value !== 'string')
36
+ throw new Error('unsupported prompt shape');
37
+ textBytes += Buffer.byteLength(value, 'utf8');
38
+ if (textBytes > MAX_BYTES)
39
+ throw new Error('capture too large');
40
+ return value;
41
+ };
42
+ const messages = [];
43
+ for (const message of options.messages) {
44
+ // Developer is not currently in dsh-llm's role union; accept its textual
45
+ // shape defensively, without copying any arbitrary message properties.
46
+ const role = message.role;
47
+ if (role !== 'system' && role !== 'developer')
48
+ continue;
49
+ const blocks = [];
50
+ for (const block of message.content) {
51
+ if (block.type === 'text')
52
+ blocks.push(text(block.text));
53
+ }
54
+ messages.push({ role, text: blocks });
55
+ }
56
+ const names = (options.tools ?? []).map(tool => safeId(tool.name));
57
+ const record = {
58
+ format: 'normalized-prompt-debug-v1',
59
+ boundary: 'llm/stream normalized GenerateOptions; before provider adapter; NOT final HTTP payload',
60
+ provider: safeId(options.provider),
61
+ model: safeId(options.model),
62
+ purpose: options.purpose === 'compaction' || options.purpose === 'session-title' ? options.purpose : undefined,
63
+ system: options.system === undefined ? undefined : text(options.system),
64
+ messages,
65
+ tools: { count: names.length, names: names.filter((name) => name !== undefined) },
66
+ // GenerateOptions has no tool_choice / parallel_tool_calls vocabulary.
67
+ // Never inspect unknown extensions, headers, URLs, or raw request objects.
68
+ };
69
+ const serialized = JSON.stringify(record, null, 2) + '\n';
70
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_BYTES)
71
+ throw new Error('capture too large');
72
+ return serialized;
73
+ }
74
+ /**
75
+ * Install optional public llm/stream observation. Disabled by default. The
76
+ * operator must create outputDir with mode 0700 and its `enabled` sentinel;
77
+ * absence creates nothing. Removing the sentinel disables subsequent captures
78
+ * without restart (already-written captures remain sensitive). Never mkdir or
79
+ * chmod a user path. Reject a whole capture over 2 MiB, never truncate it.
80
+ *
81
+ * This is normalized input at this middleware's position, NOT the final HTTP
82
+ * request; later middleware/adapters may transform it. System/developer text
83
+ * itself can contain secrets. No user/assistant/tool text, schemas, headers,
84
+ * credentials, endpoints or standard logging are included. The untouched stream
85
+ * is returned directly; no extra model calls or stream consumption occur.
86
+ */
87
+ export function installPromptDebug(ctx, options) {
88
+ const maxCalls = options.maxCalls ?? 3;
89
+ if (options.enabled !== true || typeof options.sessionId !== 'string' || options.sessionId.trim() === ''
90
+ || typeof options.outputDir !== 'string' || !isAbsolute(options.outputDir)
91
+ || !Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > PROCESS_LIMIT)
92
+ return () => { };
93
+ const outputDir = resolve(options.outputDir);
94
+ const sessionId = options.sessionId;
95
+ let calls = 0;
96
+ return ctx.on('llm/stream', (request, next) => {
97
+ try {
98
+ const budget = budgets[BUDGET_KEY] ??= { attempts: 0 };
99
+ if (request.sessionId === sessionId && calls < maxCalls && budget.attempts < PROCESS_LIMIT) {
100
+ const directory = privateDirectory(outputDir);
101
+ if (enabled(outputDir)) {
102
+ // Reserve before serializing/writing, including failures. All capture
103
+ // work is synchronous, so concurrent calls cannot overshoot either cap.
104
+ calls++;
105
+ budget.attempts++;
106
+ const data = capture(request);
107
+ if (enabled(outputDir)) {
108
+ const current = privateDirectory(outputDir);
109
+ if (current.dev !== directory.dev || current.ino !== directory.ino)
110
+ throw new Error('directory changed');
111
+ const path = join(outputDir, `prompt-${process.pid}-${randomUUID()}.json`);
112
+ const fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
113
+ try {
114
+ const file = fstatSync(fd);
115
+ const after = privateDirectory(outputDir);
116
+ if (!file.isFile() || file.nlink !== 1 || file.uid !== process.getuid?.()
117
+ || (file.mode & 0o777) !== 0o600 || after.dev !== directory.dev || after.ino !== directory.ino) {
118
+ throw new Error('unsafe capture file');
119
+ }
120
+ if (enabled(outputDir))
121
+ writeFileSync(fd, data, 'utf8');
122
+ }
123
+ finally {
124
+ closeSync(fd);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ catch {
131
+ // Debugging must never fail a model call or log prompt-bearing errors.
132
+ }
133
+ return next();
134
+ });
135
+ }
136
+ //# sourceMappingURL=debug.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.js","sourceRoot":"","sources":["../src/debug.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
+ export { PARALLEL_TOOLS_PROMPT, SECTION_NAME, SECTION_ORDER } from './prompt.js';
4
+ export declare const name = "parallel-tools-prompt";
5
+ export declare const inject: string[];
6
+ /** Stable marker so a reload never injects the reminder twice into one session. */
7
+ export declare const CONTEXT_MARKER = "[parallel-tools:scheduling]";
8
+ export declare const CONTEXT_REMINDER = "[parallel-tools:scheduling] Every response: before answering, check what else the current subgoal already has ready and issue ALL ready, independent tool calls together in this one response \u2014 same-tool calls on different targets, parallel reads, independent searches and checks. Each response is one model round trip; issuing ready work one call at a time spends one round trip per call. Hold a call only for real dependencies: its arguments need an earlier result, read-before-edit, shared-file or approval ordering. A single call is fine when nothing else is ready; never invent calls to fill a batch.";
9
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
10
+ debugEnabled: z<boolean, boolean, "defined">;
11
+ debugSessionId: z<string, string, "defined">;
12
+ debugOutputDir: z<string, string, "defined">;
13
+ debugMaxCalls: z<number, number, "defined">;
14
+ enabled: z<boolean, boolean, "defined">;
15
+ contextReminder: z<boolean, boolean, "defined">;
16
+ presetIds: z<string[], string[], "defined">;
17
+ }>>, Schemastery.ObjectT<NoInfer<{
18
+ debugEnabled: z<boolean, boolean, "defined">;
19
+ debugSessionId: z<string, string, "defined">;
20
+ debugOutputDir: z<string, string, "defined">;
21
+ debugMaxCalls: z<number, number, "defined">;
22
+ enabled: z<boolean, boolean, "defined">;
23
+ contextReminder: z<boolean, boolean, "defined">;
24
+ presetIds: z<string[], string[], "defined">;
25
+ }>>, "plain">;
26
+ export type Config = ReturnType<typeof Config>;
27
+ /** Host-only: new/resumed agent lifecycles plus reload-time re-attachment. */
28
+ export declare function apply(ctx: Context, config: Config): void;
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAMnD,OAAO,CAAC,MAAM,0BAA0B,CAAC;AAIzC,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjF,eAAO,MAAM,IAAI,0BAA0B,CAAC;AAC5C,eAAO,MAAM,MAAM,UAAmD,CAAC;AAEvE,mFAAmF;AACnF,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAC5D,eAAO,MAAM,gBAAgB,qmBAAslB,CAAC;AAEpnB,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;aAejB,CAAC;AAEH,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AA4D/C,8EAA8E;AAC9E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAiCxD"}
package/lib/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
+ import z from '@deepseek-ai/schemastery';
3
+ import { PARALLEL_TOOLS_PROMPT, SECTION_NAME, SECTION_ORDER } from './prompt.js';
4
+ import { installPromptDebug } from './debug.js';
5
+ export { PARALLEL_TOOLS_PROMPT, SECTION_NAME, SECTION_ORDER } from './prompt.js';
6
+ export const name = 'parallel-tools-prompt';
7
+ export const inject = ['agents', 'systemPrompt', 'sessionProjections'];
8
+ /** Stable marker so a reload never injects the reminder twice into one session. */
9
+ export const CONTEXT_MARKER = '[parallel-tools:scheduling]';
10
+ export const CONTEXT_REMINDER = `${CONTEXT_MARKER} Every response: before answering, check what else the current subgoal already has ready and issue ALL ready, independent tool calls together in this one response — same-tool calls on different targets, parallel reads, independent searches and checks. Each response is one model round trip; issuing ready work one call at a time spends one round trip per call. Hold a call only for real dependencies: its arguments need an earlier result, read-before-edit, shared-file or approval ordering. A single call is fine when nothing else is ready; never invent calls to fill a batch.`;
11
+ export const Config = z.object({
12
+ debugEnabled: z.boolean().default(false).description('Capture normalized LLM system prompts locally, only with an explicit session and enable sentinel. Sensitive: disable after diagnosis.'),
13
+ debugSessionId: z.string().default(''),
14
+ debugOutputDir: z.string().default(''),
15
+ debugMaxCalls: z.natural().min(1).max(10).default(3),
16
+ enabled: z.boolean().default(false).description('Enable static tool-call scheduling guidance. The bundle explicitly opts in; direct plugin use is disabled by default.'),
17
+ contextReminder: z.boolean().default(false).description('Additionally inject a one-time scheduling reminder as model-facing conversation context at the next step boundary. Persists in the session transcript; survives reloads without duplication.'),
18
+ presetIds: z.array(z.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/))
19
+ .max(32).default(['standard']).description('Exact preset IDs to target at agent creation. Root agents only; use native-tool presets, not PTC. An empty list disables all targets.'),
20
+ });
21
+ /**
22
+ * Attach or re-attach the guidance section to one already-eligible live agent.
23
+ * Idempotent via the registration guard; shared by `agent/created` and
24
+ * reload-time catch-up, so a plugin reload cannot silently drop guidance from
25
+ * agents that survive it (the old instance's disposal removed its sections).
26
+ */
27
+ function attach(ctx, agent, config, presetIds, registrations, active) {
28
+ if (registrations.has(agent))
29
+ return;
30
+ const { header } = agent.session;
31
+ // Durable child metadata survives resume; live ownership covers children
32
+ // without that metadata. parentSession is fork lineage, NOT a parent Agent.
33
+ if (header.origin === 'subagent' || (header.delegationDepth ?? 0) > 0)
34
+ return;
35
+ if (!ctx.agents.roots().includes(agent))
36
+ return;
37
+ const preset = ctx.sessionProjections.stateOf(agent.session, 'agentPreset')
38
+ ?? header.agentPreset;
39
+ if (preset === undefined || !presetIds.has(preset))
40
+ return;
41
+ // Conversation-level reminder: rides model-facing context at the nearest step
42
+ // boundary and persists in the transcript, so it stays proximal to generation.
43
+ // The transcript scan keeps reloads from stacking duplicate reminders.
44
+ if (config.contextReminder) {
45
+ try {
46
+ const already = agent.session.snapshotEvents().some(event => event.type === 'user/message' && JSON.stringify(event.data?.content ?? '').includes(CONTEXT_MARKER));
47
+ if (!already) {
48
+ agent.inject(createUserMessage({
49
+ content: [{ type: 'text', text: CONTEXT_REMINDER }],
50
+ source: { kind: 'user' },
51
+ }));
52
+ }
53
+ }
54
+ catch { /* reminder must never break attach */ }
55
+ }
56
+ const registration = {};
57
+ // Returning the disposer is essential: ctx.effect does not implicitly adopt
58
+ // arbitrary registrations executed inside its callback on another fiber.
59
+ registration.release = agent.ctx.effect(() => {
60
+ const disposeSection = agent.ctx.systemPrompt.section({
61
+ name: SECTION_NAME,
62
+ order: SECTION_ORDER,
63
+ interpolate: false,
64
+ text: PARALLEL_TOOLS_PROMPT,
65
+ });
66
+ return () => {
67
+ try {
68
+ return disposeSection();
69
+ }
70
+ finally {
71
+ registrations.delete(agent);
72
+ active.delete(registration);
73
+ }
74
+ };
75
+ });
76
+ registrations.set(agent, registration);
77
+ active.add(registration);
78
+ }
79
+ /** Host-only: new/resumed agent lifecycles plus reload-time re-attachment. */
80
+ export function apply(ctx, config) {
81
+ if (config.debugEnabled) {
82
+ installPromptDebug(ctx, { enabled: true, sessionId: config.debugSessionId,
83
+ outputDir: config.debugOutputDir, maxCalls: config.debugMaxCalls });
84
+ }
85
+ if (!config.enabled)
86
+ return;
87
+ const presetIds = new Set(config.presetIds);
88
+ const registrations = new WeakMap();
89
+ const active = new Set();
90
+ // Agent contexts are not children of this plugin's fiber. Explicitly give the
91
+ // plugin ownership of their effect disposers as well as agent-local ownership.
92
+ ctx.effect(() => async () => {
93
+ for (const registration of [...active])
94
+ await registration.release?.();
95
+ active.clear();
96
+ });
97
+ ctx.on('agent/created', ({ agent }) => {
98
+ attach(ctx, agent, config, presetIds, registrations, active);
99
+ });
100
+ // A plugin reload disposes the previous instance's section registrations, and
101
+ // the host does not replay `agent/created` for agents that stay alive. Attach
102
+ // to currently live eligible roots so guidance survives reloads; the guard
103
+ // keeps this idempotent for agents that were never affected.
104
+ ctx.effect(() => {
105
+ // Runs once at apply/reload time: re-attach to agents that survived the
106
+ // previous instance's disposal. The no-op disposer satisfies the effect
107
+ // contract; per-agent cleanup is owned by each registration instead.
108
+ for (const agent of ctx.agents.roots())
109
+ attach(ctx, agent, config, presetIds, registrations, active);
110
+ return () => { };
111
+ });
112
+ }
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,CAAC,MAAM,0BAA0B,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,OAAO,EAAE,qBAAqB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjF,MAAM,CAAC,MAAM,IAAI,GAAG,uBAAuB,CAAC;AAC5C,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,QAAQ,EAAE,cAAc,EAAE,oBAAoB,CAAC,CAAC;AAEvE,mFAAmF;AACnF,MAAM,CAAC,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAC5D,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,cAAc,kkBAAkkB,CAAC;AAEpnB,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,uIAAuI,CAAC;IAC7L,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACtC,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACtC,aAAa,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACpD,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,WAAW,CAC7C,uHAAuH,CACxH;IACD,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,WAAW,CACrD,8LAA8L,CAC/L;IACD,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC;SACzE,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CACxC,uIAAuI,CACxI;CACJ,CAAC,CAAC;AAMH;;;;;GAKG;AACH,SAAS,MAAM,CAAC,GAAY,EAAE,KAAY,EAAE,MAAc,EAAE,SAAsB,EAAE,aAA2C,EAAE,MAAyB;IACxJ,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO;IACrC,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC;IACjC,yEAAyE;IACzE,4EAA4E;IAC5E,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,MAAM,CAAC,eAAe,IAAI,CAAC,CAAC,GAAG,CAAC;QAAE,OAAO;IAC9E,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO;IAChD,MAAM,MAAM,GAAG,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,aAAa,CAAC;WACtE,MAAM,CAAC,WAAW,CAAC;IACxB,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO;IAE3D,8EAA8E;IAC9E,+EAA+E;IAC/E,uEAAuE;IACvE,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAC1D,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC;YACvG,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,KAAK,CAAC,MAAM,CAAC,iBAAiB,CAAC;oBAC7B,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;oBACnD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;iBACzB,CAAC,CAAC,CAAC;YACN,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAC,sCAAsC,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,YAAY,GAAiB,EAAE,CAAC;IACtC,4EAA4E;IAC5E,yEAAyE;IACzE,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;QAC3C,MAAM,cAAc,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC;YACpD,IAAI,EAAE,YAAY;YAClB,KAAK,EAAE,aAAa;YACpB,WAAW,EAAE,KAAK;YAClB,IAAI,EAAE,qBAAqB;SAC5B,CAAC,CAAC;QACH,OAAO,GAAG,EAAE;YACV,IAAI,CAAC;gBACH,OAAO,cAAc,EAAE,CAAC;YAC1B,CAAC;oBAAS,CAAC;gBACT,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC5B,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;IACvC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAC3B,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,MAAc;IAChD,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,kBAAkB,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,cAAc;YACvE,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE,QAAQ,EAAE,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO;IAE5B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,aAAa,GAAG,IAAI,OAAO,EAAuB,CAAC;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAgB,CAAC;IAEvC,8EAA8E;IAC9E,+EAA+E;IAC/E,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE;QAC1B,KAAK,MAAM,YAAY,IAAI,CAAC,GAAG,MAAM,CAAC;YAAE,MAAM,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC;QACvE,MAAM,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,EAAE,KAAK,EAAE,EAAa,EAAE;QAC/C,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,8EAA8E;IAC9E,8EAA8E;IAC9E,2EAA2E;IAC3E,6DAA6D;IAC7D,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;QACd,wEAAwE;QACxE,wEAAwE;QACxE,qEAAqE;QACrE,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE;YAAE,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC;QACrG,OAAO,GAAG,EAAE,GAAE,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** Stable prompt bytes: no runtime interpolation, tool inventory, or per-turn state. */
2
+ export declare const SECTION_NAME = "parallel-tools:guidance";
3
+ export declare const SECTION_ORDER = 1050;
4
+ export declare const PARALLEL_TOOLS_PROMPT = "Tool-call scheduling:\n- Before each response, briefly check what else the current subgoal already has ready: same-tool calls on different targets, parallel reads, independent searches and checks. Issue every ready, independent call together in THIS response.\n- Hold a call only when its arguments depend on an earlier result, or ordering is required (read-before-edit, shared file, approval flow). A later combined decision does not make independent collection sequential.\n- A single call is correct when nothing else is ready; never invent or duplicate calls to fill a batch. Keep scheduling internal: no planning-only turn, no shell wrappers or delegation just to simulate batching.\n- Several calls in one response do not force simultaneous execution; the host scheduler, permissions, and all safety rules still apply.";
5
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,eAAO,MAAM,YAAY,4BAA4B,CAAC;AACtD,eAAO,MAAM,aAAa,OAAO,CAAC;AAElC,eAAO,MAAM,qBAAqB,6zBAIsG,CAAC"}
package/lib/prompt.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Stable prompt bytes: no runtime interpolation, tool inventory, or per-turn state. */
2
+ export const SECTION_NAME = 'parallel-tools:guidance';
3
+ export const SECTION_ORDER = 1050;
4
+ export const PARALLEL_TOOLS_PROMPT = `Tool-call scheduling:
5
+ - Before each response, briefly check what else the current subgoal already has ready: same-tool calls on different targets, parallel reads, independent searches and checks. Issue every ready, independent call together in THIS response.
6
+ - Hold a call only when its arguments depend on an earlier result, or ordering is required (read-before-edit, shared file, approval flow). A later combined decision does not make independent collection sequential.
7
+ - A single call is correct when nothing else is ready; never invent or duplicate calls to fill a batch. Keep scheduling internal: no planning-only turn, no shell wrappers or delegation just to simulate batching.
8
+ - Several calls in one response do not force simultaneous execution; the host scheduler, permissions, and all safety rules still apply.`;
9
+ //# sourceMappingURL=prompt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC;AAElC,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;wIAImG,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "dsh-parallel-tools-prompt",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.1-rc.2",
4
+ "description": "Plan ready tool-call batches to reduce model round trips in DeepSeek Harness",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/lloydzhou/dsh-parallel-tools-prompt.git"
8
+ },
9
+ "homepage": "https://github.com/lloydzhou/dsh-parallel-tools-prompt#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/lloydzhou/dsh-parallel-tools-prompt/issues"
12
+ },
13
+ "publishConfig": { "access": "public", "tag": "next" },
14
+ "type": "module",
15
+ "license": "MIT",
16
+ "main": "./lib/index.js",
17
+ "types": "./lib/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./lib/index.d.ts",
21
+ "import": "./lib/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": ["lib", "cordis.patch.yml", "README.md", "LICENSE", "scripts/analyze-session.mjs"],
26
+ "engines": {
27
+ "node": ">=24.9.0",
28
+ "dsh": ">=0.2.1-rc.3"
29
+ },
30
+ "dsh": {
31
+ "bundle": {
32
+ "patch": "./cordis.patch.yml"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "vitest run",
39
+ "prepack": "npm run build"
40
+ },
41
+ "peerDependencies": {
42
+ "@deepseek-ai/cordis": "4.0.5",
43
+ "@deepseek-ai/schemastery": "3.18.5",
44
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.3",
45
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.3",
46
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.3",
47
+ "@deepseek-ai/dsh-session-projection": "^0.2.1-rc.3",
48
+ "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.3"
49
+ },
50
+ "devDependencies": {
51
+ "@deepseek-ai/cordis": "4.0.5",
52
+ "@deepseek-ai/schemastery": "3.18.5",
53
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.3",
54
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.3",
55
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.3",
56
+ "@deepseek-ai/dsh-session-projection": "^0.2.1-rc.3",
57
+ "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.3",
58
+ "@deepseek-ai/dsh-session": "^0.2.1-rc.3",
59
+ "@deepseek-ai/dsh-scope": "^0.2.1-rc.3",
60
+ "@deepseek-ai/cordis-plugin-include": "1.0.10",
61
+ "@deepseek-ai/cordis-plugin-loader": "1.0.6",
62
+ "@types/node": "^24.0.0",
63
+ "@types/js-yaml": "^4.0.9",
64
+ "js-yaml": "^4.1.1",
65
+ "typescript": "^5.9.3",
66
+ "vitest": "^4.1.0"
67
+ }
68
+ }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Read-only descriptive metrics for explicitly named DSH session logs.
4
+ * No session discovery, network, content output, replay, or deduplication.
5
+ * Counts include inherited/seed events and repeated input files. These are NOT
6
+ * billed API counts, concurrency measurements, LLM latency, or causal savings.
7
+ * JSONL is read with readline; zstd is synchronous and limited to 64 MiB both
8
+ * compressed and expanded. Requires Node's built-in zstd support (Node >=24.9).
9
+ * Only the event envelope and metric-bearing fields are validated, not the full
10
+ * extensible DSH schema. Unknown event types are counted but otherwise ignored.
11
+ */
12
+ import { open } from 'node:fs/promises';
13
+ import { createInterface } from 'node:readline';
14
+ import { Readable } from 'node:stream';
15
+ import { pathToFileURL } from 'node:url';
16
+ import { resolve } from 'node:path';
17
+ import { zstdDecompressSync } from 'node:zlib';
18
+
19
+ const MAX_ZSTD_BYTES = 64 * 1024 * 1024;
20
+ const HELP = `Usage: node scripts/analyze-session.mjs FILE.jsonl [FILE.jsonl.zstd ...]
21
+ Explicit files only; no default scan, network, writes, or content/path output.
22
+ Outputs per-file (1-based fileIndex) and pooled aggregate JSON metrics.
23
+ Batch sizes come only from assistant/message content tool-call blocks.
24
+ Tool execution events (tool/call) and assistant/attempt events are separate.
25
+ Multi-call proportion uses tool-bearing responses as its denominator.
26
+ Missing denominators and unreported output-token totals are null, not zero.
27
+ Raw log counts include inherited history and repeated files; no deduplication.
28
+ Not billed API counts, actual concurrency, LLM latency, or causal A/B savings.
29
+ JSONL streams via readline. Zstd buffers synchronously: 64 MiB input/output cap.
30
+ Unknown events are ignored for metrics; this is not full schema validation.
31
+ `;
32
+
33
+ class InputError extends Error {}
34
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
35
+ const count = value => Number.isSafeInteger(value) && value >= 0;
36
+ function requireValid(condition) {
37
+ if (!condition) throw new InputError('invalid event structure');
38
+ }
39
+ function accumulator() {
40
+ return {
41
+ eventCount: 0, modelResponseEvents: 0, modelAttemptEvents: 0,
42
+ toolExecutionEvents: 0, toolResultEvents: 0, toolCallBlocks: 0,
43
+ toolBearingResponses: 0, multiCallResponses: 0,
44
+ knownResponseCount: 0, unknownResponseCount: 0, reportedTotal: 0,
45
+ batchSizes: new Map(),
46
+ };
47
+ }
48
+ function add(state, event) {
49
+ requireValid(object(event) && typeof event.type === 'string' && event.type.length > 0);
50
+ if (event.type === 'session') {
51
+ requireValid(count(event.version) && typeof event.id === 'string' && count(event.createdAt));
52
+ return;
53
+ }
54
+ requireValid(object(event.data) && count(event.seq) && count(event.time));
55
+ state.eventCount++;
56
+ const { data } = event;
57
+ if (event.type === 'assistant/message') {
58
+ requireValid(object(data.message) && data.message.role === 'assistant' && Array.isArray(data.message.content));
59
+ let calls = 0;
60
+ for (const block of data.message.content) {
61
+ requireValid(object(block) && typeof block.type === 'string');
62
+ if (block.type === 'tool-call') {
63
+ requireValid(typeof block.id === 'string' && typeof block.name === 'string' && typeof block.arguments === 'string');
64
+ calls++;
65
+ }
66
+ }
67
+ state.modelResponseEvents++;
68
+ state.toolCallBlocks += calls;
69
+ if (calls > 0) {
70
+ state.toolBearingResponses++;
71
+ if (calls > 1) state.multiCallResponses++;
72
+ state.batchSizes.set(calls, (state.batchSizes.get(calls) ?? 0) + 1);
73
+ }
74
+ if (data.usage !== undefined) requireValid(object(data.usage));
75
+ const tokens = data.usage?.outputTokens;
76
+ if (tokens === undefined) state.unknownResponseCount++;
77
+ else {
78
+ requireValid(count(tokens));
79
+ state.knownResponseCount++;
80
+ state.reportedTotal += tokens;
81
+ requireValid(Number.isSafeInteger(state.reportedTotal));
82
+ }
83
+ } else if (event.type === 'assistant/attempt') {
84
+ requireValid(Array.isArray(data.stream));
85
+ state.modelAttemptEvents++;
86
+ } else if (event.type === 'tool/call') {
87
+ requireValid(typeof data.callId === 'string' && typeof data.name === 'string' && typeof data.arguments === 'string');
88
+ state.toolExecutionEvents++;
89
+ } else if (event.type === 'tool/result') {
90
+ requireValid(object(data.message) && data.message.role === 'tool');
91
+ state.toolResultEvents++;
92
+ }
93
+ }
94
+ function finish(state) {
95
+ const { batchSizes, knownResponseCount, unknownResponseCount, reportedTotal, ...metrics } = state;
96
+ const n = state.toolBearingResponses;
97
+ let median = null;
98
+ if (n) {
99
+ const left = Math.floor((n - 1) / 2);
100
+ const right = Math.floor(n / 2);
101
+ let seen = 0;
102
+ let sum = 0;
103
+ for (const [size, frequency] of [...batchSizes].sort((a, b) => a[0] - b[0])) {
104
+ if (left >= seen && left < seen + frequency) sum += size;
105
+ if (right >= seen && right < seen + frequency) sum += size;
106
+ seen += frequency;
107
+ }
108
+ median = sum / 2;
109
+ }
110
+ return {
111
+ ...metrics,
112
+ multiCallResponseProportion: n ? state.multiCallResponses / n : null,
113
+ meanCallsPerToolBearingResponse: n ? state.toolCallBlocks / n : null,
114
+ medianCallsPerToolBearingResponse: median,
115
+ outputTokens: { reportedTotal: knownResponseCount ? reportedTotal : null, knownResponseCount, unknownResponseCount },
116
+ };
117
+ }
118
+
119
+ /** Pure metrics over iterable public SessionEvent objects (optional session header). */
120
+ export function analyzeEvents(events) {
121
+ const state = accumulator();
122
+ for (const event of events) add(state, event);
123
+ return finish(state);
124
+ }
125
+
126
+ async function inputStream(file) {
127
+ if (!file.endsWith('.jsonl') && !file.endsWith('.zstd')) throw new InputError('expected .jsonl or .zstd file');
128
+ const handle = await open(file, 'r');
129
+ let transferred = false;
130
+ try {
131
+ const stat = await handle.stat();
132
+ if (!stat.isFile()) throw new InputError('expected a regular file');
133
+ if (file.endsWith('.zstd')) {
134
+ if (stat.size > MAX_ZSTD_BYTES) throw new InputError('zstd input exceeds 64 MiB limit');
135
+ const compressed = await handle.readFile();
136
+ if (compressed.length > MAX_ZSTD_BYTES) throw new InputError('zstd input exceeds 64 MiB limit');
137
+ let decoded;
138
+ try { decoded = zstdDecompressSync(compressed, { maxOutputLength: MAX_ZSTD_BYTES }); }
139
+ catch { throw new InputError('invalid zstd or expanded data exceeds 64 MiB limit'); }
140
+ return Readable.from([decoded]);
141
+ }
142
+ // The verified handle is reused: no second path lookup or implicit scanning.
143
+ const stream = handle.createReadStream({ autoClose: true });
144
+ transferred = true;
145
+ return stream;
146
+ } finally {
147
+ if (!transferred) await handle.close();
148
+ }
149
+ }
150
+
151
+ /** Analyze only supplied files; never include their names or content in results/errors. */
152
+ export async function analyzeFiles(files) {
153
+ if (!Array.isArray(files) || !files.length) throw new InputError('supply at least one explicit session file');
154
+ const aggregate = accumulator();
155
+ const results = [];
156
+ for (const [index, file] of files.entries()) {
157
+ let lineNumber = 0;
158
+ let input;
159
+ let lines;
160
+ const local = accumulator();
161
+ try {
162
+ input = await inputStream(file);
163
+ lines = createInterface({ input, crlfDelay: Infinity });
164
+ for await (const line of lines) {
165
+ lineNumber++;
166
+ if (!line.trim()) continue;
167
+ let event;
168
+ try { event = JSON.parse(line); }
169
+ catch { throw new InputError('malformed JSON'); }
170
+ add(local, event);
171
+ add(aggregate, event);
172
+ }
173
+ } catch (error) {
174
+ const reason = error instanceof InputError ? error.message : 'unable to read session';
175
+ throw new InputError(`file ${index + 1}${lineNumber ? `, line ${lineNumber}` : ''}: ${reason}`);
176
+ } finally {
177
+ lines?.close();
178
+ input?.destroy();
179
+ }
180
+ results.push({ fileIndex: index + 1, metrics: finish(local) });
181
+ }
182
+ return { files: results, aggregate: finish(aggregate) };
183
+ }
184
+
185
+ async function main(args) {
186
+ if (args.length === 1 && (args[0] === '--help' || args[0] === '-h')) {
187
+ process.stdout.write(HELP);
188
+ return;
189
+ }
190
+ try {
191
+ if (args.some(arg => arg.startsWith('-'))) throw new InputError('unknown option; use --help (prefix dash-leading paths with ./)');
192
+ process.stdout.write(`${JSON.stringify(await analyzeFiles(args), null, 2)}\n`);
193
+ } catch (error) {
194
+ process.stderr.write(`analyze-session: ${error instanceof InputError ? error.message : 'analysis failed'}\n`);
195
+ process.exitCode = 1;
196
+ }
197
+ }
198
+
199
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
200
+ await main(process.argv.slice(2));
201
+ }