dsh-fast-mode 1.5.22 → 1.5.23
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/CHANGELOG.md +4 -0
- package/bench/mechanism.py +129 -0
- package/bench/patchshare.py +95 -0
- package/bench/reminders.py +112 -0
- package/bench/textshare.py +150 -0
- package/index.js +1 -1
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.23(打包:把量测脚本一起发出去)
|
|
4
|
+
|
|
5
|
+
- 1.5.22 的文档让读者用 `bench/mechanism.py` / `textshare.py` / `reminders.py` / `patchshare.py` 复算结论,但 npm 包里只带了 `block_score.py`,从 npm 装的人复算不了。现在这四个只读分析脚本一起进包(都是 stdlib、只读 `~/.dsh/sessions`)。无行为变更。
|
|
6
|
+
|
|
3
7
|
## 1.5.22(只加量测与文档:机制拆解、纪律份额、同配置重复性;默认行为不变)
|
|
4
8
|
|
|
5
9
|
- **机制拆解落地成工具**:新增 `bench/mechanism.py`,把 168 对「同 tag、同任务、同 pair、只差 arm」的会话转录逐项数出来(步数、调用数、并行率、只叙述步、prose/参数/推理字符)。结论写进 BENCHMARK:并行的两条口径都是 1.00(即并行**不是**杠杆);步数 0.83 / 调用 0.79 / 参数 0.84 / prose 0.80 / 推理 0.75,全都落在 0.75–0.84——插件是「每样都少一点」,不是砍某一种输出。转录里推理字符占输出字符的 99.8 %(每会话中位 47.5 k vs 92),所以只盯可见文字优化、天花板是 0.2 %。
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""What actually changes between two arms: steps, tool calls, batching, prose.
|
|
3
|
+
|
|
4
|
+
The wall-clock win of fast mode is a step-count win (see BENCHMARK.md). This
|
|
5
|
+
script opens the retained transcripts behind matched runs and reports *which*
|
|
6
|
+
count moved, so the mechanism can be named instead of inferred:
|
|
7
|
+
|
|
8
|
+
messages assistant messages = steps as the runner counts them
|
|
9
|
+
calls tool calls emitted
|
|
10
|
+
calls/msg batching width
|
|
11
|
+
multiShare share of messages carrying >= 2 tool calls
|
|
12
|
+
textOnlyShare share of messages carrying no tool call (narration-only steps)
|
|
13
|
+
proseShare visible prose chars / (prose + tool-argument chars)
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 bench/mechanism.py --arm off --arm def15 # every matched pair
|
|
17
|
+
python3 bench/mechanism.py --arm off --arm def15 --tag DEF_MIG
|
|
18
|
+
python3 bench/mechanism.py --arm off --arm def15 --json
|
|
19
|
+
python3 bench/mechanism.py --list-arms # what is on disk
|
|
20
|
+
|
|
21
|
+
Runs are the session directories named `suite-<tag>-<task>-p<pair>-<arm>`;
|
|
22
|
+
only (tag, task, pair) groups that contain **both** arms are used, so the task
|
|
23
|
+
mix cancels exactly like the wall-clock block score does.
|
|
24
|
+
"""
|
|
25
|
+
import argparse
|
|
26
|
+
import collections
|
|
27
|
+
import glob
|
|
28
|
+
import importlib.util
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import statistics as st
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
35
|
+
SESS = os.path.expanduser('~/.dsh/sessions')
|
|
36
|
+
|
|
37
|
+
spec = importlib.util.spec_from_file_location('textshare', os.path.join(HERE, 'textshare.py'))
|
|
38
|
+
textshare = importlib.util.module_from_spec(spec)
|
|
39
|
+
spec.loader.exec_module(textshare)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def collect_all():
|
|
43
|
+
out = []
|
|
44
|
+
for d in glob.glob(os.path.join(SESS, '--*dsh-runs-suite-*')):
|
|
45
|
+
name = os.path.basename(d).strip('-')
|
|
46
|
+
m = textshare.RUN_RE.search(name)
|
|
47
|
+
if not m:
|
|
48
|
+
continue
|
|
49
|
+
out.append((m.group('tag'), m.group('task'), int(m.group('pair')), m.group('arm'), d, name))
|
|
50
|
+
out.sort(key=lambda x: x[5])
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def stats_for(run):
|
|
55
|
+
text = textshare.read_transcript(run[4])
|
|
56
|
+
prose, args, msgs, calls, multi, textonly = textshare.tally(text)
|
|
57
|
+
if not msgs:
|
|
58
|
+
return None
|
|
59
|
+
return {'prose': prose, 'args': args, 'messages': msgs, 'calls': calls,
|
|
60
|
+
'multi': multi, 'textonly': textonly,
|
|
61
|
+
'callsPerMsg': calls / msgs,
|
|
62
|
+
'multiShare': multi / msgs,
|
|
63
|
+
'textOnlyShare': textonly / msgs,
|
|
64
|
+
'proseShare': prose / (prose + args) if prose + args else 0.0}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
ap = argparse.ArgumentParser()
|
|
69
|
+
ap.add_argument('--arm', action='append', default=[])
|
|
70
|
+
ap.add_argument('--tag', default='')
|
|
71
|
+
ap.add_argument('--limit', type=int, default=0, help='max matched groups; 0 = all')
|
|
72
|
+
ap.add_argument('--json', action='store_true')
|
|
73
|
+
ap.add_argument('--list-arms', action='store_true')
|
|
74
|
+
a = ap.parse_args()
|
|
75
|
+
|
|
76
|
+
runs = collect_all()
|
|
77
|
+
if a.list_arms:
|
|
78
|
+
c = collections.Counter(r[3] for r in runs)
|
|
79
|
+
for arm, n in c.most_common():
|
|
80
|
+
print(f'{arm:12} {n:5} runs')
|
|
81
|
+
return 0
|
|
82
|
+
arms = a.arm or ['off', 'def15']
|
|
83
|
+
if len(arms) != 2:
|
|
84
|
+
print('give exactly two --arm values', file=sys.stderr)
|
|
85
|
+
return 2
|
|
86
|
+
|
|
87
|
+
groups = collections.defaultdict(dict)
|
|
88
|
+
for tag, task, pair, arm, d, name in runs:
|
|
89
|
+
if a.tag and tag != a.tag:
|
|
90
|
+
continue
|
|
91
|
+
if arm not in arms:
|
|
92
|
+
continue
|
|
93
|
+
groups[(tag, task, pair)][arm] = (tag, task, pair, arm, d, name)
|
|
94
|
+
|
|
95
|
+
cells = []
|
|
96
|
+
for key in sorted(groups):
|
|
97
|
+
g = groups[key]
|
|
98
|
+
if not all(x in g for x in arms):
|
|
99
|
+
continue
|
|
100
|
+
row = {x: stats_for(g[x]) for x in arms}
|
|
101
|
+
if any(v is None for v in row.values()):
|
|
102
|
+
continue
|
|
103
|
+
cells.append(row)
|
|
104
|
+
if a.limit:
|
|
105
|
+
cells = cells[:a.limit]
|
|
106
|
+
if not cells:
|
|
107
|
+
print('no matched groups with transcripts; nothing to compute', file=sys.stderr)
|
|
108
|
+
return 1
|
|
109
|
+
|
|
110
|
+
keys = ['messages', 'calls', 'callsPerMsg', 'multiShare', 'textOnlyShare', 'proseShare']
|
|
111
|
+
med = {x: {k: st.median([c[x][k] for c in cells]) for k in keys} for x in arms}
|
|
112
|
+
summary = {'arms': arms, 'tag': a.tag or '*', 'groups': len(cells)}
|
|
113
|
+
for k in keys:
|
|
114
|
+
summary[k] = {'median': {x: med[x][k] for x in arms}}
|
|
115
|
+
ratios = [c[arms[1]][k] / c[arms[0]][k] for c in cells if c[arms[0]][k]]
|
|
116
|
+
summary[k]['medianRatio'] = st.median(ratios) if ratios else None
|
|
117
|
+
if a.json:
|
|
118
|
+
print(json.dumps(summary, indent=2))
|
|
119
|
+
return 0
|
|
120
|
+
print(f"matched groups: {len(cells)} (arm A = {arms[0]}, arm B = {arms[1]})")
|
|
121
|
+
print(f"{'metric':14} {arms[0]:>10} {arms[1]:>10} {'B/A':>7}")
|
|
122
|
+
for k in keys:
|
|
123
|
+
b, r = med[arms[1]][k], summary[k]['medianRatio']
|
|
124
|
+
print(f"{k:14} {med[arms[0]][k]:10.3f} {b:10.3f} {(r if r is not None else float('nan')):7.3f}")
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == '__main__':
|
|
129
|
+
sys.exit(main())
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""How much of the reuse lever was switched off by patch-shaped edits.
|
|
3
|
+
|
|
4
|
+
1.5.19 marked a mutation whose target it could not locate as "uncertain" and then
|
|
5
|
+
stopped offering check-reuse suggestions for the rest of that session. Patch calls
|
|
6
|
+
(`apply_patch` / `patch`) name their targets inside the diff body, so before 1.5.20
|
|
7
|
+
any session that edited once with a patch paid that cost for every later check.
|
|
8
|
+
This counts, from retained transcripts, how common that shape is: sessions that
|
|
9
|
+
call a patch tool, and the tool calls that follow it in those sessions.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python3 bench/patchshare.py # all local sessions
|
|
13
|
+
python3 bench/patchshare.py --limit 0 # explicit: all
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import glob
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
|
|
24
|
+
SESS = os.path.expanduser('~/.dsh/sessions')
|
|
25
|
+
PATCH_CALLS = {'apply_patch', 'patch', 'apply_diff', 'edit_patch'}
|
|
26
|
+
WRITE_CALLS = {'write', 'write_file', 'create_file', 'edit', 'multi_edit'} | PATCH_CALLS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sessions(limit):
|
|
30
|
+
out = []
|
|
31
|
+
for d in sorted(glob.glob(os.path.join(SESS, '--*'))):
|
|
32
|
+
files = glob.glob(os.path.join(d, 'session-*', 'session.v4.jsonl.zstd'))
|
|
33
|
+
if files:
|
|
34
|
+
out.append((d, files[0]))
|
|
35
|
+
return out[:limit] if limit else out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def tally(path):
|
|
39
|
+
p = subprocess.run(['zstd', '-dc', path], capture_output=True)
|
|
40
|
+
if p.returncode != 0:
|
|
41
|
+
return None
|
|
42
|
+
calls = []
|
|
43
|
+
for line in p.stdout.decode('utf-8', 'replace').splitlines():
|
|
44
|
+
try:
|
|
45
|
+
o = json.loads(line)
|
|
46
|
+
except Exception: # noqa: BLE001 - a torn line is not fatal
|
|
47
|
+
continue
|
|
48
|
+
if o.get('type') != 'assistant/message':
|
|
49
|
+
continue
|
|
50
|
+
for part in (o.get('data') or {}).get('message', {}).get('content') or []:
|
|
51
|
+
if isinstance(part, dict) and part.get('type') == 'tool-call':
|
|
52
|
+
calls.append(part.get('toolName') or part.get('name') or '')
|
|
53
|
+
if not calls:
|
|
54
|
+
return None
|
|
55
|
+
first_patch = next((i for i, n in enumerate(calls) if n in PATCH_CALLS), None)
|
|
56
|
+
return {
|
|
57
|
+
'calls': len(calls),
|
|
58
|
+
'patch_calls': sum(1 for n in calls if n in PATCH_CALLS),
|
|
59
|
+
'write_calls': sum(1 for n in calls if n in WRITE_CALLS),
|
|
60
|
+
'first_patch': first_patch,
|
|
61
|
+
'after_patch': 0 if first_patch is None else len(calls) - first_patch - 1,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
ap = argparse.ArgumentParser()
|
|
67
|
+
ap.add_argument('--limit', type=int, default=60, help='sessions, most recent first; 0 = all')
|
|
68
|
+
a = ap.parse_args()
|
|
69
|
+
rows = []
|
|
70
|
+
for d, f in reversed(sessions(a.limit)):
|
|
71
|
+
r = tally(f)
|
|
72
|
+
if r:
|
|
73
|
+
r['session'] = os.path.basename(d)[:24]
|
|
74
|
+
rows.append(r)
|
|
75
|
+
if not rows:
|
|
76
|
+
print('no sessions')
|
|
77
|
+
return
|
|
78
|
+
with_patch = [r for r in rows if r['patch_calls'] > 0]
|
|
79
|
+
calls = sum(r['calls'] for r in rows)
|
|
80
|
+
pcalls = sum(r['patch_calls'] for r in rows)
|
|
81
|
+
after = sum(r['after_patch'] for r in with_patch)
|
|
82
|
+
print(f"sessions {len(rows)} calls {calls}")
|
|
83
|
+
print(f"sessions that patched {len(with_patch)} ({100 * len(with_patch) / len(rows):.0f} %)")
|
|
84
|
+
print(f"patch calls {pcalls} ({100 * pcalls / calls:.1f} % of calls)")
|
|
85
|
+
print(f"calls after the first patch, in patching sessions {after} "
|
|
86
|
+
f"({100 * after / calls:.1f} % of all calls)")
|
|
87
|
+
closed = [r for r in with_patch if r['after_patch'] > 0]
|
|
88
|
+
print(f"patching sessions with work still to do after the patch {len(closed)}")
|
|
89
|
+
med = sorted(r['after_patch'] for r in closed)
|
|
90
|
+
if med:
|
|
91
|
+
print(f" median calls still to do after the first patch: {med[len(med) // 2]}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == '__main__':
|
|
95
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Which scheduler reminders actually reached the model, and what followed.
|
|
3
|
+
|
|
4
|
+
A reminder is only a lever if it is delivered. This counts, over retained
|
|
5
|
+
transcripts, every directive text the plugin injected (`Auto schedule: ...`),
|
|
6
|
+
grouped by kind, plus how often the quoted text shows up inside the model's own
|
|
7
|
+
prose (the model echoing the instruction back is not delivery).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python3 bench/reminders.py # suite sessions, default cap
|
|
11
|
+
python3 bench/reminders.py --limit 0 # every retained session
|
|
12
|
+
python3 bench/reminders.py --dir '--*dsh-runs-suite-*'
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import collections
|
|
19
|
+
import glob
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
|
|
24
|
+
SESS = os.path.expanduser('~/.dsh/sessions')
|
|
25
|
+
KINDS = (
|
|
26
|
+
('that exact check already passed', 'reuse-check'),
|
|
27
|
+
('this turn is long enough that prose', 'output-discipline'),
|
|
28
|
+
('batch now. Put every independent tool call', 'batch-now'),
|
|
29
|
+
('files changed since the last run', 'write-and-verify'),
|
|
30
|
+
('reached the step budget', 'step-budget'),
|
|
31
|
+
('consecutive steps produced nothing new', 'change-approach'),
|
|
32
|
+
('consecutive steps produced nothing', 'change-approach'),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def transcripts(pattern, limit):
|
|
37
|
+
out = []
|
|
38
|
+
for d in sorted(glob.glob(os.path.join(SESS, pattern))):
|
|
39
|
+
for f in glob.glob(os.path.join(d, 'session-*', 'session.v4.jsonl.zstd')):
|
|
40
|
+
out.append((d, f))
|
|
41
|
+
return out[:limit] if limit else out
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def scan(path):
|
|
45
|
+
p = subprocess.run(['zstd', '-dc', path], capture_output=True)
|
|
46
|
+
if p.returncode != 0:
|
|
47
|
+
return None
|
|
48
|
+
text = p.stdout.decode('utf-8', 'replace')
|
|
49
|
+
counts = collections.Counter()
|
|
50
|
+
echoed = collections.Counter()
|
|
51
|
+
delivered = 0
|
|
52
|
+
for needle, kind in KINDS:
|
|
53
|
+
n = text.count(needle)
|
|
54
|
+
if n == 0:
|
|
55
|
+
continue
|
|
56
|
+
counts[kind] += n
|
|
57
|
+
# A directive is delivered as its own message part; the model repeating the
|
|
58
|
+
# phrase inside an assistant text block is an echo, so count both and report
|
|
59
|
+
# the delivery count only when it is separable.
|
|
60
|
+
for line in text.splitlines():
|
|
61
|
+
if 'Auto schedule:' not in line:
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
o = json.loads(line)
|
|
65
|
+
except Exception: # noqa: BLE001 - a torn line is not fatal
|
|
66
|
+
continue
|
|
67
|
+
role = o.get('type')
|
|
68
|
+
data = o.get('data') or {}
|
|
69
|
+
msg = data.get('message') or data
|
|
70
|
+
body = ' '.join(part.get('text') or '' for part in msg.get('content') or []
|
|
71
|
+
if isinstance(part, dict) and part.get('type') == 'text')
|
|
72
|
+
if 'Auto schedule:' not in body:
|
|
73
|
+
continue
|
|
74
|
+
for needle, kind in KINDS:
|
|
75
|
+
if needle in body:
|
|
76
|
+
if role == 'assistant/message':
|
|
77
|
+
echoed[kind] += 1
|
|
78
|
+
else:
|
|
79
|
+
delivered += 1
|
|
80
|
+
break
|
|
81
|
+
return counts, echoed, delivered
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
ap = argparse.ArgumentParser()
|
|
86
|
+
ap.add_argument('--limit', type=int, default=0, help='transcripts; 0 = all')
|
|
87
|
+
ap.add_argument('--dir', default='--*dsh-runs-suite-*')
|
|
88
|
+
a = ap.parse_args()
|
|
89
|
+
files = transcripts(a.dir, a.limit)
|
|
90
|
+
total = collections.Counter()
|
|
91
|
+
echo = collections.Counter()
|
|
92
|
+
delivered = 0
|
|
93
|
+
with_any = 0
|
|
94
|
+
scanned = 0
|
|
95
|
+
for d, f in files:
|
|
96
|
+
r = scan(f)
|
|
97
|
+
if not r:
|
|
98
|
+
continue
|
|
99
|
+
scanned += 1
|
|
100
|
+
counts, echoed, n = r
|
|
101
|
+
delivered += n
|
|
102
|
+
total.update(counts)
|
|
103
|
+
echo.update(echoed)
|
|
104
|
+
with_any += 1 if sum(counts.values()) > 0 else 0
|
|
105
|
+
print(f'transcripts {scanned} with any directive {with_any} '
|
|
106
|
+
f'({100 * with_any / max(scanned, 1):.0f} %) delivered {delivered}')
|
|
107
|
+
for kind, n in total.most_common():
|
|
108
|
+
print(f' {kind:18} raw {n:4} echoed-in-prose {echo[kind]:4}')
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
main()
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Visible assistant prose vs tool-call argument characters.
|
|
3
|
+
|
|
4
|
+
Recomputes the claim "visible assistant prose is only ~1.2 % of (prose + tool
|
|
5
|
+
arguments)" from retained session transcripts, so the number in the docs has a
|
|
6
|
+
command behind it.
|
|
7
|
+
|
|
8
|
+
Counting unit is one `assistant/message` record of a session transcript:
|
|
9
|
+
* numerator = sum of len(text) over `content` parts of type `text`;
|
|
10
|
+
* denominator = numerator + sum of len(arguments) over parts of type `tool-call`.
|
|
11
|
+
Reasoning parts, tool *results*, and user/system messages are excluded; a tool-call
|
|
12
|
+
part's `arguments` is the raw JSON string the model emitted, so the ratio compares
|
|
13
|
+
what the model wrote out of band with what it said in prose.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python3 bench/textshare.py # 40 def15 sessions, fixed rule
|
|
17
|
+
python3 bench/textshare.py --all # every def15 session on disk
|
|
18
|
+
python3 bench/textshare.py --arm disc-only --limit 0 --tag D2B_MIG
|
|
19
|
+
python3 bench/textshare.py --json # machine-readable summary
|
|
20
|
+
|
|
21
|
+
Sampling rule (deterministic, no RNG): all session directories whose run-directory
|
|
22
|
+
name matches `suite-<tag>-<task>-p<pair>-<arm>` are collected, sorted by that
|
|
23
|
+
directory name, and the first --limit are used (--limit 0 = all). The same corpus
|
|
24
|
+
therefore always yields the same number.
|
|
25
|
+
"""
|
|
26
|
+
import argparse
|
|
27
|
+
import glob
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import re
|
|
31
|
+
import statistics as st
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
|
|
35
|
+
SESS = os.path.expanduser('~/.dsh/sessions')
|
|
36
|
+
RUN_RE = re.compile(r'suite-(?P<tag>[A-Za-z0-9_]+)-(?P<task>\w+)-p(?P<pair>\d+)'
|
|
37
|
+
r'-(?P<arm>[A-Za-z0-9_]+)$')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def collect(arm, tag, limit):
|
|
41
|
+
"""[(session_dir, run_name)] for one arm, in a fixed sorted order."""
|
|
42
|
+
out = []
|
|
43
|
+
for d in glob.glob(os.path.join(SESS, '--*dsh-runs-suite-*')):
|
|
44
|
+
name = os.path.basename(d).strip('-')
|
|
45
|
+
m = RUN_RE.search(name)
|
|
46
|
+
if not m or m.group('arm') != arm:
|
|
47
|
+
continue
|
|
48
|
+
if tag and m.group('tag') != tag:
|
|
49
|
+
continue
|
|
50
|
+
out.append((d, name))
|
|
51
|
+
out.sort(key=lambda x: x[1])
|
|
52
|
+
return out[:limit] if limit else out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_transcript(d):
|
|
56
|
+
files = glob.glob(os.path.join(d, 'session-*', 'session.v4.jsonl.zstd'))
|
|
57
|
+
for f in files:
|
|
58
|
+
p = subprocess.run(['zstd', '-dc', f], capture_output=True)
|
|
59
|
+
if p.returncode == 0:
|
|
60
|
+
return p.stdout.decode('utf-8', 'replace')
|
|
61
|
+
return ''
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def tally(text):
|
|
65
|
+
"""(prose chars, tool-arg chars, messages, tool calls, multi-call messages)."""
|
|
66
|
+
prose = args = msgs = calls = multi = textonly = 0
|
|
67
|
+
for line in text.splitlines():
|
|
68
|
+
try:
|
|
69
|
+
o = json.loads(line)
|
|
70
|
+
except Exception: # noqa: BLE001 - a torn transcript line is not fatal
|
|
71
|
+
continue
|
|
72
|
+
if o.get('type') != 'assistant/message':
|
|
73
|
+
continue
|
|
74
|
+
msgs += 1
|
|
75
|
+
this = 0
|
|
76
|
+
for part in (o.get('data') or {}).get('message', {}).get('content') or []:
|
|
77
|
+
if not isinstance(part, dict):
|
|
78
|
+
continue
|
|
79
|
+
if part.get('type') == 'text':
|
|
80
|
+
prose += len(part.get('text') or '')
|
|
81
|
+
elif part.get('type') == 'tool-call':
|
|
82
|
+
calls += 1
|
|
83
|
+
this += 1
|
|
84
|
+
args += len(part.get('arguments') or '')
|
|
85
|
+
if this >= 2:
|
|
86
|
+
multi += 1
|
|
87
|
+
elif this == 0:
|
|
88
|
+
textonly += 1
|
|
89
|
+
return prose, args, msgs, calls, multi, textonly
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main():
|
|
93
|
+
ap = argparse.ArgumentParser()
|
|
94
|
+
ap.add_argument('--arm', default='def15', help='arm name to sample (default def15)')
|
|
95
|
+
ap.add_argument('--tag', default='', help='restrict to one block tag, e.g. D2B_MIG')
|
|
96
|
+
ap.add_argument('--limit', type=int, default=40, help='sessions, first in sorted order; 0 = all')
|
|
97
|
+
ap.add_argument('--json', action='store_true')
|
|
98
|
+
a = ap.parse_args()
|
|
99
|
+
|
|
100
|
+
rows = collect(a.arm, a.tag, a.limit)
|
|
101
|
+
per = []
|
|
102
|
+
for d, name in rows:
|
|
103
|
+
prose, args, msgs, calls, multi, textonly = tally(read_transcript(d))
|
|
104
|
+
if msgs:
|
|
105
|
+
per.append({'run': name, 'prose': prose, 'args': args, 'messages': msgs,
|
|
106
|
+
'calls': calls, 'multi': multi, 'textonly': textonly,
|
|
107
|
+
'share': prose / (prose + args) if prose + args else 0})
|
|
108
|
+
if not per:
|
|
109
|
+
print(f'no transcripts for arm={a.arm} tag={a.tag or "*"}; nothing to compute')
|
|
110
|
+
return 1
|
|
111
|
+
prose = sum(r['prose'] for r in per)
|
|
112
|
+
args = sum(r['args'] for r in per)
|
|
113
|
+
total = prose + args
|
|
114
|
+
shares = sorted(r['share'] for r in per)
|
|
115
|
+
summary = {
|
|
116
|
+
'arm': a.arm, 'tag': a.tag or '*', 'limit': a.limit,
|
|
117
|
+
'sessions': len(per), 'messages': sum(r['messages'] for r in per),
|
|
118
|
+
'calls': sum(r['calls'] for r in per),
|
|
119
|
+
'multiCallMessages': sum(r['multi'] for r in per),
|
|
120
|
+
'multiCallShare': (sum(r['multi'] for r in per) /
|
|
121
|
+
max(sum(r['messages'] for r in per), 1)),
|
|
122
|
+
'textOnlyMessages': sum(r['textonly'] for r in per),
|
|
123
|
+
'textOnlyShare': (sum(r['textonly'] for r in per) /
|
|
124
|
+
max(sum(r['messages'] for r in per), 1)),
|
|
125
|
+
'proseChars': prose, 'toolArgChars': args, 'totalChars': total,
|
|
126
|
+
'share': prose / total if total else None,
|
|
127
|
+
'medianShare': st.median(shares), 'p10': shares[int(0.1 * (len(shares) - 1))],
|
|
128
|
+
'p90': shares[int(0.9 * (len(shares) - 1))],
|
|
129
|
+
'rule': f'sorted by run name, first {a.limit or "all"} of arm={a.arm}' +
|
|
130
|
+
(f' tag={a.tag}' if a.tag else ''),
|
|
131
|
+
'command': 'python3 bench/textshare.py' + ('' if a.arm == 'def15' else f' --arm {a.arm}') +
|
|
132
|
+
(f' --tag {a.tag}' if a.tag else '') +
|
|
133
|
+
('' if a.limit == 40 else f' --limit {a.limit}'),
|
|
134
|
+
}
|
|
135
|
+
if a.json:
|
|
136
|
+
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
|
137
|
+
return 0
|
|
138
|
+
print(f"arm {a.arm} tag {a.tag or '*'} {summary['rule']}")
|
|
139
|
+
print(f" sessions {summary['sessions']} messages {summary['messages']} "
|
|
140
|
+
f"tool calls {summary['calls']}")
|
|
141
|
+
print(f" prose {prose:,} chars tool args {args:,} chars total {total:,} chars")
|
|
142
|
+
print(f" prose share {100 * summary['share']:.2f} % "
|
|
143
|
+
f"(median {100 * summary['medianShare']:.2f} %, "
|
|
144
|
+
f"p10 {100 * summary['p10']:.2f} %, p90 {100 * summary['p90']:.2f} %)")
|
|
145
|
+
print(f" command: {summary['command']}")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == '__main__':
|
|
150
|
+
sys.exit(main())
|
package/index.js
CHANGED
|
@@ -97,7 +97,7 @@ const TAG = '[fast-mode]'
|
|
|
97
97
|
* Shipped identity of this build. Traced into the debug log so a stale install is
|
|
98
98
|
* visible at a glance, and pinned by a test against `package.json`.
|
|
99
99
|
*/
|
|
100
|
-
export const VERSION = '1.5.
|
|
100
|
+
export const VERSION = '1.5.23'
|
|
101
101
|
const GLOBAL_AGENT = '__global__'
|
|
102
102
|
|
|
103
103
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fast-mode",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.23",
|
|
4
4
|
"description": "DeepSeek Harness fast mode: same thinking effort, fewer round trips. An adaptive parallel tool fan-out, output discipline that never trims the answer, and a cancellable stall guard. Never changes reasoning effort, the model, or the request body. Provider-agnostic, one dependency (@deepseek-ai/schemastery).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"CHANGELOG.md",
|
|
40
40
|
"BENCHMARK.md",
|
|
41
41
|
"bench/block_score.py",
|
|
42
|
+
"bench/mechanism.py",
|
|
43
|
+
"bench/textshare.py",
|
|
44
|
+
"bench/reminders.py",
|
|
45
|
+
"bench/patchshare.py",
|
|
42
46
|
"docs/",
|
|
43
47
|
"LICENSE"
|
|
44
48
|
],
|