harness-everything 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +1 -0
  4. package/CHANGELOG.md +47 -0
  5. package/PRIVACY.md +9 -0
  6. package/benchmarks/fixtures/system-one-holdout-reviews.json +18 -18
  7. package/benchmarks/fixtures/system-one-holdout.json +9 -9
  8. package/benchmarks/fixtures/system-one-intent-holdout-draft.json +2228 -0
  9. package/benchmarks/fixtures/system-one-intent-holdout.json +16509 -0
  10. package/benchmarks/fixtures/system-one-intent-reviews.json +2723 -0
  11. package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/README.md +75 -0
  12. package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/report.json +10832 -0
  13. package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/README.md +72 -0
  14. package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/report.json +10817 -0
  15. package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/README.md +63 -0
  16. package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/report.json +10817 -0
  17. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/README.md +64 -0
  18. package/benchmarks/results/system-one/intent-ngram-v1-2026-09-24/report.json +15177 -0
  19. package/benchmarks/results/system-one/labeler-holdout-check-2026-09-24/report.json +1104 -0
  20. package/benchmarks/results/system-one/labeler-holdout-check-codex-2026-09-24/report.json +1104 -0
  21. package/benchmarks/results/system-one/labeler-holdout-check-codex-tier3-2026-09-24/report.json +1104 -0
  22. package/benchmarks/results/system-one/labeler-holdout-check-intent-2026-09-24/report.json +2976 -0
  23. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-005-steps.json +6125 -0
  24. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-haiku-sonnet-escalation.json +6344 -0
  25. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report-scaled-margin-escalation.json +6206 -0
  26. package/benchmarks/results/system-one/labeler-holdout-check-intent-scores-haiku-2026-09-24/report.json +6103 -0
  27. package/benchmarks/results/system-one/laya-ft-2026-09-25/README.md +83 -0
  28. package/benchmarks/results/system-one/laya-ft-2026-09-25/report.json +71 -0
  29. package/benchmarks/results/system-one/laya-ft-2026-09-25/shadow-report.json +44 -0
  30. package/benchmarks/results/system-one/laya-zs-2026-09-25/README.md +68 -0
  31. package/benchmarks/results/system-one/laya-zs-2026-09-25/report.json +173 -0
  32. package/ci/mechanism-34-observations.test.js +550 -0
  33. package/ci/mechanism-system-one-collect.test.js +16 -0
  34. package/ci/mechanism-system-one-evaluation.test.js +17 -0
  35. package/ci/mechanism-system-one-intent.test.js +406 -0
  36. package/ci/mechanism-system-one-label.test.js +27 -0
  37. package/ci/mechanism-system-one-laya.test.js +45 -0
  38. package/ci/mechanism-system-one-ngram.test.js +161 -0
  39. package/ci/mechanism-system-one-relevance.test.js +112 -0
  40. package/ci/mechanism-system-one-resident.test.js +16 -0
  41. package/ci/mechanism-system-one-train.test.js +12 -0
  42. package/ci/system-one-laya-calibrate-test.py +65 -0
  43. package/ci/system-one-laya-export-test.py +155 -0
  44. package/ci/system-one-laya-label-test.py +201 -0
  45. package/ci/system-one-laya-perintent-test.py +60 -0
  46. package/ci/system-one-laya-provider-test.py +115 -0
  47. package/ci/system-one-laya-train-test.py +93 -0
  48. package/ci/system-one-ngram-parity.py +19 -0
  49. package/ci/system-one-train-test.py +6 -0
  50. package/docs/routing.md +25 -0
  51. package/docs/system-one-corpus.md +31 -3
  52. package/docs/system-one-intent-gates.md +102 -0
  53. package/docs/system-one-intent.md +173 -0
  54. package/docs/system-one-laya-training.md +38 -0
  55. package/docs/system-one-observations.md +206 -0
  56. package/docs/system-one-routing.md +102 -5
  57. package/docs/system-one-suggestion-gates.md +292 -0
  58. package/docs/system-one-training.md +69 -7
  59. package/harness-everything/SKILL.md +1 -1
  60. package/harness-everything/scripts/kernel-router.js +10 -0
  61. package/harness-everything/scripts/system-one/catalogs.js +35 -0
  62. package/harness-everything/scripts/system-one/contract.js +45 -1
  63. package/harness-everything/scripts/system-one/evaluate.js +118 -21
  64. package/harness-everything/scripts/system-one/intent.js +22 -0
  65. package/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  66. package/harness-everything/scripts/system-one/ngram.js +84 -0
  67. package/harness-everything/scripts/system-one/provider.js +24 -7
  68. package/harness-everything/scripts/system-one/resident.js +8 -3
  69. package/harness-everything/scripts/system-one/router.js +1 -1
  70. package/hooks/hooks.json +40 -0
  71. package/hooks/scripts/codex-user-prompt.js +39 -0
  72. package/hooks/scripts/lib/observations.js +581 -0
  73. package/hooks/scripts/observation-hook.js +25 -0
  74. package/opencode-plugin/plugin.json +1 -1
  75. package/package.json +1 -1
  76. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  77. package/plugins/harness-everything/hooks/hooks.json +35 -2
  78. package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +39 -0
  79. package/plugins/harness-everything/hooks/scripts/lib/observations.js +581 -0
  80. package/plugins/harness-everything/hooks/scripts/observation-hook.js +25 -0
  81. package/plugins/harness-everything/plugin.json +1 -1
  82. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  83. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +10 -0
  84. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/catalogs.js +35 -0
  85. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/contract.js +45 -1
  86. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +118 -21
  87. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/intent.js +22 -0
  88. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/laya_adapter.py +141 -0
  89. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/ngram.js +84 -0
  90. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +24 -7
  91. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/resident.js +8 -3
  92. package/plugins/harness-everything/skills/harness-everything/scripts/system-one/router.js +1 -1
  93. package/sbom.cdx.json +7 -7
  94. package/scripts/evaluate-system-one.js +32 -13
  95. package/scripts/system-one-calibrate-laya.py +165 -0
  96. package/scripts/system-one-calibrate.js +42 -13
  97. package/scripts/system-one-collect.js +7 -3
  98. package/scripts/system-one-compare-laya.py +156 -0
  99. package/scripts/system-one-corpus.js +22 -15
  100. package/scripts/system-one-eval-laya.js +112 -0
  101. package/scripts/system-one-export-laya.py +197 -0
  102. package/scripts/system-one-label-laya.py +243 -0
  103. package/scripts/system-one-label.js +192 -37
  104. package/scripts/system-one-observations-export.js +106 -0
  105. package/scripts/system-one-observations-review.js +86 -0
  106. package/scripts/system-one-perintent-laya.py +134 -0
  107. package/scripts/system-one-train-laya.py +344 -0
  108. package/scripts/system-one-train-ngram.py +181 -0
  109. package/scripts/system-one-train.py +14 -1
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ """Per-intent Bernoulli evaluation for Laya relevance scores (#255 Phase 3).
3
+
4
+ Each intent is its own binary decision with its own threshold, fit to the
5
+ owner's secondary band (teacher score >= 0.4). No simplex, no single winner:
6
+ this matches how owner intents actually look (about 2.5 secondaries per
7
+ prompt) and how Laya noul heads natively behave (independent P(true)).
8
+
9
+ Honest estimate via 2-fold CV; the shipped taus artifact is fit on all rows
10
+ and labeled in-sample. Stdlib only; runs in seconds on saved predictions.
11
+ """
12
+ import argparse
13
+ import hashlib
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
19
+ 'review', 'test', 'docs', 'plan', 'investigate']
20
+
21
+ GRID = [round(x / 100, 2) for x in range(5, 100, 5)]
22
+
23
+
24
+ def binarize(scores, band=0.4):
25
+ """Intents whose score reaches the positive band."""
26
+ return {i for i in INTENTS if scores.get(i, 0.0) >= band}
27
+
28
+
29
+ def sweep_threshold(scores, truth):
30
+ """Best F1 threshold on paired lists; ties break toward silence."""
31
+ best = (0.95, 0.0, 0.0, 0.0)
32
+ for tau in GRID:
33
+ tp = sum(1 for s, t in zip(scores, truth) if s >= tau and t)
34
+ fp = sum(1 for s, t in zip(scores, truth) if s >= tau and not t)
35
+ fn = sum(1 for s, t in zip(scores, truth) if s < tau and t)
36
+ p = tp / (tp + fp) if (tp + fp) else 0.0
37
+ r = tp / (tp + fn) if (tp + fn) else 0.0
38
+ f = 2 * p * r / (p + r) if (p + r) else 0.0
39
+ if (f, tau) > (best[3], best[0]):
40
+ best = (tau, p, r, f)
41
+ return best
42
+
43
+
44
+ def micro(per):
45
+ """Micro-averaged P/R/F1 over {intent: (tp, fp, fn)}; empty scores zero."""
46
+ tp = sum(v[0] for v in per.values())
47
+ fp = sum(v[1] for v in per.values())
48
+ fn = sum(v[2] for v in per.values())
49
+ p = tp / (tp + fp) if (tp + fp) else 0.0
50
+ r = tp / (tp + fn) if (tp + fn) else 0.0
51
+ return {'precision': p, 'recall': r, 'f1': 2 * p * r / (p + r) if (p + r) else 0.0}
52
+
53
+
54
+ def evaluate(ids, preds, truth_sets, taus):
55
+ """Multi-label metrics for one id list under fixed taus."""
56
+ per = {i: [0, 0, 0] for i in INTENTS}
57
+ abstain, fires_total, truth_total, exact = 0, 0, 0, 0
58
+ for pid in ids:
59
+ fired = {i for i in INTENTS if preds[pid].get(i, 0.0) >= taus[i]}
60
+ truth = truth_sets[pid]
61
+ fires_total += len(fired)
62
+ truth_total += len(truth)
63
+ abstain += not fired
64
+ exact += fired == truth
65
+ for i in INTENTS:
66
+ if i in fired and i in truth:
67
+ per[i][0] += 1
68
+ elif i in fired:
69
+ per[i][1] += 1
70
+ elif i in truth:
71
+ per[i][2] += 1
72
+ n = len(ids)
73
+ return {'n': n, 'micro': micro({i: tuple(v) for i, v in per.items()}),
74
+ 'abstainRate': abstain / n if n else 0.0,
75
+ 'meanFires': fires_total / n if n else 0.0,
76
+ 'meanTruth': truth_total / n if n else 0.0,
77
+ 'exactSetMatch': exact / n if n else 0.0,
78
+ 'perIntent': {i: {'tau': taus[i], 'tp': per[i][0], 'fp': per[i][1], 'fn': per[i][2],
79
+ 'support': per[i][0] + per[i][2]} for i in INTENTS}}
80
+
81
+
82
+ def fit_taus(ids, preds, truth_sets):
83
+ """Max-F1 threshold per intent on the given ids."""
84
+ taus = {}
85
+ for i in INTENTS:
86
+ taus[i] = sweep_threshold([preds[pid].get(i, 0.0) for pid in ids],
87
+ [i in truth_sets[pid] for pid in ids])[0]
88
+ return taus
89
+
90
+
91
+ def main(argv=None):
92
+ parser = argparse.ArgumentParser(description=__doc__)
93
+ parser.add_argument('--preds', required=True, help='saved predictions json {id: {intent: score}}')
94
+ parser.add_argument('--teacher', required=True, help='teacher scores jsonl')
95
+ parser.add_argument('--band', type=float, default=0.4, help='teacher positive band')
96
+ parser.add_argument('--out', required=True, help='report json path')
97
+ parser.add_argument('--taus', required=True, help='threshold artifact json path')
98
+ args = parser.parse_args(argv)
99
+ preds = json.loads(Path(args.preds).read_text(encoding='utf-8'))
100
+ teacher = {r['id']: r for r in
101
+ (json.loads(line) for line in Path(args.teacher).read_text(encoding='utf-8').splitlines() if line.strip())}
102
+ ids = sorted(set(preds) & set(teacher))
103
+ truth_sets = {pid: binarize(teacher[pid]['scores'], args.band) for pid in ids}
104
+ halves = ([i for k, i in enumerate(ids) if k % 2 == 0], [i for k, i in enumerate(ids) if k % 2 == 1])
105
+ cv = [evaluate(halves[1], preds, truth_sets, fit_taus(halves[0], preds, truth_sets)),
106
+ evaluate(halves[0], preds, truth_sets, fit_taus(halves[1], preds, truth_sets))]
107
+ full_taus = fit_taus(ids, preds, truth_sets)
108
+ in_sample = evaluate(ids, preds, truth_sets, full_taus)
109
+ report = {'n': len(ids), 'band': args.band,
110
+ 'cvMicroF1': sum(c['micro']['f1'] for c in cv) / 2,
111
+ 'cv': [{'micro': c['micro'], 'abstainRate': c['abstainRate'],
112
+ 'meanFires': c['meanFires'], 'exactSetMatch': c['exactSetMatch']} for c in cv],
113
+ 'inSample': {'micro': in_sample['micro'], 'abstainRate': in_sample['abstainRate'],
114
+ 'meanFires': in_sample['meanFires'], 'meanTruth': in_sample['meanTruth'],
115
+ 'exactSetMatch': in_sample['exactSetMatch'],
116
+ 'perIntent': in_sample['perIntent']}}
117
+ Path(args.out).write_text(json.dumps(report, indent=2), encoding='utf-8')
118
+ Path(args.taus).write_text(json.dumps(
119
+ {'taus': full_taus, 'band': args.band, 'inSample': True,
120
+ 'predsSha256': hashlib.sha256(Path(args.preds).read_bytes()).hexdigest(),
121
+ 'teacherSha256': hashlib.sha256(Path(args.teacher).read_bytes()).hexdigest()},
122
+ indent=2), encoding='utf-8')
123
+ m = report['inSample']['micro']
124
+ print(json.dumps({'n': len(ids), 'cvF1': round(report['cvMicroF1'], 3),
125
+ 'inSampleF1': round(m['f1'], 3), 'inSampleP': round(m['precision'], 3),
126
+ 'inSampleR': round(m['recall'], 3),
127
+ 'fires': round(in_sample['meanFires'], 2),
128
+ 'truth': round(in_sample['meanTruth'], 2),
129
+ 'abstain': round(in_sample['abstainRate'], 3)}))
130
+ return 0
131
+
132
+
133
+ if __name__ == '__main__':
134
+ sys.exit(main())
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env python3
2
+ """Single-device RLCD fine-tuning for Laya (#255 Phase 3).
3
+
4
+ Faithful single-process port of the upstream Kaggle DDP recipe
5
+ (notebooks/laya_finetune_typed_decisions_2xT4_kaggle.ipynb, train_ddp.py):
6
+ same GRPO + soft cross-entropy objective, same calibration holdout seed,
7
+ same temperature fit, same artifact layout. Differences: no NCCL/DDP;
8
+ cuda uses fp16+scaler, mps/cpu train fp32 without a scaler.
9
+
10
+ Input is the reviewable items jsonl from system-one-export-laya.py.
11
+ Output is a loadable checkpoint dir (model.safetensors + encoder/ +
12
+ tokenizer/ + rl_agent_config.json). Heavy deps import lazily so contract
13
+ tests run on stdlib.
14
+ """
15
+ import argparse
16
+ import json
17
+ import random
18
+ import sys
19
+ import time
20
+ from pathlib import Path
21
+
22
+ try:
23
+ import torch
24
+ _TORCH = True
25
+ except ImportError:
26
+ torch = None
27
+ _TORCH = False
28
+
29
+ CALIB_SEED = 20260922
30
+ CALIB_MAX = 400
31
+ INTENTS = ['explain', 'discuss', 'git', 'fix', 'edit', 'feature', 'refactor',
32
+ 'review', 'test', 'docs', 'plan', 'investigate']
33
+
34
+
35
+ def need_torch():
36
+ if not _TORCH:
37
+ raise RuntimeError('torch is required for training')
38
+
39
+
40
+ def intent_weights(intents, table):
41
+ """Per-item loss multipliers by training intent; unlisted intents weigh 1."""
42
+ import math # noqa: PLC0415
43
+ table = dict(table or {})
44
+ for intent, weight in table.items():
45
+ if intent not in INTENTS or not isinstance(weight, (int, float)) \
46
+ or not math.isfinite(weight) or weight <= 0:
47
+ raise ValueError(f'intent-weights: bad entry {intent!r}:{weight!r}')
48
+ return [float(table.get(intent, 1.0)) for intent in intents]
49
+
50
+
51
+ def calib_split(items, seed=CALIB_SEED, frac=0.1):
52
+ """Deterministic calibration split, grouped by prompt when promptId exists.
53
+
54
+ Calibration keeps one copy per promptId+intent identity. Oversampled copies
55
+ survive only in training groups, preventing exact-duplicate leakage and
56
+ calibration reweighting. Callers without promptId retain per-item behavior.
57
+ """
58
+ groups = {}
59
+ group_order = []
60
+ for index, item in enumerate(items):
61
+ prompt_id = item.get('promptId')
62
+ key = ('prompt', str(prompt_id)) if prompt_id is not None else ('item', index)
63
+ if key not in groups:
64
+ groups[key] = []
65
+ group_order.append(key)
66
+ groups[key].append(item)
67
+
68
+ def calib_rows(group):
69
+ if not group or group[0].get('promptId') is None:
70
+ return list(group)
71
+ seen, rows = set(), []
72
+ for item in group:
73
+ identity = (item.get('promptId'), item.get('intent'))
74
+ if identity in seen:
75
+ continue
76
+ seen.add(identity)
77
+ rows.append(item)
78
+ return rows
79
+
80
+ base_groups = {key: calib_rows(group) for key, group in groups.items()}
81
+ base_total = sum(len(group) for group in base_groups.values())
82
+ if base_total == 0:
83
+ return [], []
84
+ n_calib = max(1, min(CALIB_MAX, base_total // 10 if frac == 0.1 else int(base_total * frac)))
85
+
86
+ shuffled = list(group_order)
87
+ random.Random(seed).shuffle(shuffled)
88
+ calib_groups, calib_count = set(), 0
89
+ for key in shuffled:
90
+ size = len(base_groups[key])
91
+ if calib_count == 0 and size > n_calib:
92
+ calib_groups.add(key)
93
+ break
94
+ if calib_count + size <= n_calib:
95
+ calib_groups.add(key)
96
+ calib_count += size
97
+ if calib_count == n_calib:
98
+ break
99
+
100
+ train, calib = [], []
101
+ for key in group_order:
102
+ if key in calib_groups:
103
+ calib.extend(base_groups[key])
104
+ else:
105
+ train.extend(groups[key])
106
+ return train, calib
107
+
108
+
109
+ def collate(items, pad_id):
110
+ """Batch upstream-format items into padded tensors."""
111
+ need_torch()
112
+ n, max_len = len(items), max(len(it['ids']) for it in items)
113
+ kmax = max(len(it['markers']) for it in items)
114
+ ids = torch.full((n, max_len), pad_id, dtype=torch.long)
115
+ att = torch.zeros((n, max_len), dtype=torch.long)
116
+ mpos = torch.zeros((n, kmax), dtype=torch.long)
117
+ mmask = torch.zeros((n, kmax), dtype=torch.bool)
118
+ target = torch.zeros((n, kmax), dtype=torch.float32)
119
+ for i, it in enumerate(items):
120
+ ids[i, :len(it['ids'])] = torch.tensor(it['ids'])
121
+ att[i, :len(it['ids'])] = 1
122
+ k = len(it['markers'])
123
+ mpos[i, :k] = torch.tensor(it['markers'])
124
+ mmask[i, :k] = True
125
+ target[i, :len(it['target'])] = torch.tensor(it['target'], dtype=torch.float32)
126
+ return {'input_ids': ids, 'attention_mask': att, 'marker_pos': mpos, 'marker_mask': mmask,
127
+ 'target': target,
128
+ 'qtype': torch.tensor([it['qtype'] for it in items]),
129
+ 'label': torch.tensor([it['label'] for it in items])}
130
+
131
+
132
+ def fit_one_temp(sel):
133
+ """Fit one temperature per question type with LBFGS (upstream recipe)."""
134
+ need_torch()
135
+ if len(sel) < 10:
136
+ return 1.0
137
+ kmax = max(len(z) for z, _ in sel)
138
+ Z = torch.full((len(sel), kmax), -1e4)
139
+ T = torch.zeros((len(sel), kmax))
140
+ for i, (z, t) in enumerate(sel):
141
+ Z[i, :len(z)] = torch.tensor(z)
142
+ T[i, :len(t)] = torch.tensor(t, dtype=torch.float32)
143
+ log_t = torch.zeros(1, requires_grad=True)
144
+ opt = torch.optim.LBFGS([log_t], lr=0.1, max_iter=100)
145
+
146
+ def closure():
147
+ opt.zero_grad()
148
+ loss = -(T * torch.log_softmax(Z / log_t.exp(), -1)).sum(-1).mean()
149
+ loss.backward()
150
+ return loss
151
+
152
+ opt.step(closure)
153
+ return float(torch.clamp(log_t.exp(), 0.1, 10.0).item())
154
+
155
+
156
+ def pick_device(name):
157
+ need_torch()
158
+ if name == 'auto':
159
+ if torch.cuda.is_available():
160
+ return 'cuda'
161
+ if hasattr(torch, 'backends') and torch.backends.mps.is_available():
162
+ return 'mps'
163
+ return 'cpu'
164
+ return name
165
+
166
+
167
+ def train(args):
168
+ need_torch()
169
+ from safetensors.torch import load_file, save_file # noqa: PLC0415
170
+ from transformers import AutoTokenizer # noqa: PLC0415
171
+ from laya.agent import _fix_tokenizer_config # noqa: PLC0415
172
+ from laya.common import build_model, proper_reward # noqa: PLC0415
173
+
174
+ device = pick_device(args.device)
175
+ use_amp = device == 'cuda'
176
+ out_dir = Path(args.out)
177
+ out_dir.mkdir(parents=True, exist_ok=True)
178
+
179
+ items = [json.loads(line) for line in Path(args.items).read_text(encoding='utf-8').splitlines() if line.strip()]
180
+ train_items, calib_items = calib_split(items, seed=args.calib_seed, frac=args.calib_frac)
181
+ weights_table = json.loads(args.intent_weights)
182
+ intent_weights([], weights_table) # validate early, before GPU time
183
+ print(json.dumps({'trainItems': len(train_items), 'calibItems': len(calib_items), 'device': device,
184
+ 'intentWeights': weights_table}), flush=True)
185
+
186
+ from huggingface_hub import snapshot_download # noqa: PLC0415
187
+ model_dir = Path(snapshot_download(args.checkpoint))
188
+ base = model_dir / args.subfolder if args.subfolder else model_dir
189
+ _fix_tokenizer_config(str(base))
190
+ with open(base / 'rl_agent_config.json', encoding='utf-8') as fh:
191
+ cfg = json.load(fh)
192
+ cfg['max_tokens_per_batch'] = 4096
193
+ cfg['max_len'] = 1024
194
+ cfg['head_max_len'] = 256
195
+ tok = AutoTokenizer.from_pretrained(str(base / 'tokenizer'))
196
+ model = build_model(cfg, encoder_dir=str(base / 'encoder'))
197
+ model.load_state_dict(load_file(str(base / 'model.safetensors')), strict=True)
198
+ model.encoder.gradient_checkpointing_enable(gradient_checkpointing_kwargs={'use_reentrant': False})
199
+ model.head_checkpointing = True
200
+ model.to(device)
201
+ model.train()
202
+
203
+ enc_params = [p for n, p in model.named_parameters() if 'encoder.' in n]
204
+ head_params = [p for n, p in model.named_parameters() if 'encoder.' not in n]
205
+ optimizer = torch.optim.AdamW([
206
+ {'params': enc_params, 'lr': args.lr_encoder},
207
+ {'params': head_params, 'lr': args.lr_head},
208
+ ], weight_decay=0.01)
209
+ updates_per_epoch = max(1, len(train_items) // (args.micro_batch * args.grad_accum))
210
+ scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
211
+ optimizer, T_max=max(1, updates_per_epoch * args.epochs), eta_min=1e-6)
212
+ scaler = torch.amp.GradScaler('cuda', enabled=use_amp)
213
+
214
+ t0 = time.time()
215
+ for epoch in range(args.epochs):
216
+ rnd = random.Random(42 + epoch)
217
+ rnd.shuffle(train_items)
218
+ sigma = args.sigma_start + (args.sigma_end - args.sigma_start) * (epoch / max(1, args.epochs - 1))
219
+ epoch_loss, n_batches = 0.0, 0
220
+ optimizer.zero_grad(set_to_none=True)
221
+ accum = 0
222
+ for b in range(0, len(train_items), args.micro_batch):
223
+ chunk = train_items[b:b + args.micro_batch]
224
+ batch = collate(chunk, tok.pad_token_id)
225
+ ctx = torch.amp.autocast('cuda', dtype=torch.float16) if use_amp else torch.amp.autocast('cpu', enabled=False)
226
+ with ctx:
227
+ logits, act = model(batch['input_ids'].to(device), batch['attention_mask'].to(device),
228
+ batch['marker_pos'].to(device), batch['marker_mask'].to(device),
229
+ batch['qtype'].to(device))
230
+ logits = logits.float()
231
+ mask = batch['marker_mask'].to(device)
232
+ k = mask.sum(-1, keepdim=True).float()
233
+ target = batch['target'].to(device)
234
+ eps = torch.randn((args.group_size,) + logits.shape, device=device) * sigma * mask
235
+ eps = (eps - eps.sum(-1, keepdim=True) / k) * mask
236
+ z = logits.detach().unsqueeze(0) + eps
237
+ q = torch.softmax(z.masked_fill(~mask, -1e4), -1)
238
+ with torch.no_grad():
239
+ r = proper_reward(q, target.unsqueeze(0), batch['qtype'].to(device), mask,
240
+ w_sph=0.75, w_rps=1.0)
241
+ adv = r - r.mean(0, keepdim=True)
242
+ adv = adv / (adv.std() + 1e-6)
243
+ logp = -(((z - logits.unsqueeze(0)) ** 2) * mask).sum(-1) / (2 * sigma ** 2)
244
+ sample_w = torch.tensor(
245
+ intent_weights([it.get('intent') for it in chunk], weights_table),
246
+ dtype=torch.float32, device=device)
247
+ loss_rl = (-(adv * logp).mean(0) * sample_w).mean()
248
+ loss_ce = ((-(target * torch.log_softmax(logits.masked_fill(~mask, -1e4), -1)).sum(-1)) * sample_w).mean()
249
+ loss = (loss_rl + loss_ce) / args.grad_accum + 0.0 * act.sum()
250
+ scaler.scale(loss).backward()
251
+ accum += 1
252
+ if accum % args.grad_accum == 0 or (b + args.micro_batch) >= len(train_items):
253
+ scaler.unscale_(optimizer)
254
+ torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
255
+ scaler.step(optimizer)
256
+ scaler.update()
257
+ scheduler.step()
258
+ optimizer.zero_grad(set_to_none=True)
259
+ epoch_loss += loss.item() * args.grad_accum
260
+ n_batches += 1
261
+ if n_batches % 50 == 0:
262
+ print(json.dumps({'epoch': epoch + 1, 'step': n_batches,
263
+ 'loss': round(loss.item() * args.grad_accum, 4),
264
+ 'reward': round(r.mean().item(), 3)}), flush=True)
265
+ ckpt = out_dir / 'checkpoint_latest'
266
+ ckpt.mkdir(exist_ok=True)
267
+ ckpt_sd = {k: v.half().contiguous().cpu() for k, v in model.state_dict().items()}
268
+ save_file(ckpt_sd, str(ckpt / 'model.safetensors'))
269
+ model.encoder.config.save_pretrained(str(ckpt / 'encoder'))
270
+ tok.save_pretrained(str(ckpt / 'tokenizer'))
271
+ (ckpt / 'checkpoint_meta.json').write_text(json.dumps(
272
+ {'epoch': epoch + 1, 'totalEpochs': args.epochs,
273
+ 'avgLoss': epoch_loss / max(1, n_batches)}), encoding='utf-8')
274
+ print(json.dumps({'epochDone': epoch + 1, 'avgLoss': round(epoch_loss / max(1, n_batches), 4),
275
+ 'elapsedS': round(time.time() - t0, 1)}), flush=True)
276
+ if device in ('cuda', 'mps'):
277
+ getattr(torch, device).empty_cache()
278
+
279
+ print(json.dumps({'fittingTemperatures': True}), flush=True)
280
+ del optimizer, scaler, scheduler
281
+ if device == 'cuda':
282
+ torch.cuda.empty_cache()
283
+ model.eval()
284
+ calib_preds = []
285
+ with torch.no_grad():
286
+ for c in range(0, len(calib_items), 16):
287
+ chunk = calib_items[c:c + 16]
288
+ cb = collate(chunk, tok.pad_token_id)
289
+ ctx = torch.amp.autocast('cuda', dtype=torch.float16) if use_amp else torch.amp.autocast('cpu', enabled=False)
290
+ with ctx:
291
+ l_sub, _ = model(cb['input_ids'].to(device), cb['attention_mask'].to(device),
292
+ cb['marker_pos'].to(device), cb['marker_mask'].to(device),
293
+ cb['qtype'].to(device))
294
+ l_np = l_sub.float().cpu().numpy()
295
+ for r, it in enumerate(chunk):
296
+ k = len(it['markers'])
297
+ calib_preds.append((it['qtype'], l_np[r, :k], it['target']))
298
+ fitted = [1.2, 1.2, 1.2]
299
+ try:
300
+ for qt in range(3):
301
+ sel = [(z, t) for q_type, z, t in calib_preds if q_type == qt]
302
+ if sel:
303
+ fitted[qt] = fit_one_temp(sel)
304
+ except Exception as exc: # noqa: BLE001
305
+ print(json.dumps({'temperatureFallback': str(exc)}), flush=True)
306
+ sd = {k: v.half().contiguous().cpu() for k, v in model.state_dict().items()}
307
+ save_file(sd, str(out_dir / 'model.safetensors'))
308
+ model.encoder.config.save_pretrained(str(out_dir / 'encoder'))
309
+ tok.save_pretrained(str(out_dir / 'tokenizer'))
310
+ cfg['fine_tuned'] = True
311
+ cfg['model_name'] = args.model_name
312
+ cfg['temperature'] = fitted
313
+ cfg.pop('temperature_by_options', None)
314
+ (out_dir / 'rl_agent_config.json').write_text(json.dumps(cfg, indent=2), encoding='utf-8')
315
+ print(json.dumps({'done': True, 'out': str(out_dir), 'temperatures': [round(t, 3) for t in fitted],
316
+ 'elapsedS': round(time.time() - t0, 1)}), flush=True)
317
+ return 0
318
+
319
+
320
+ def main(argv=None):
321
+ parser = argparse.ArgumentParser(description=__doc__)
322
+ parser.add_argument('--items', required=True, help='items jsonl from system-one-export-laya.py')
323
+ parser.add_argument('--out', required=True, help='checkpoint output dir')
324
+ parser.add_argument('--checkpoint', default='convaiinnovations/laya')
325
+ parser.add_argument('--subfolder', default='multilingual')
326
+ parser.add_argument('--model-name', default='laya-harness-intent-s1')
327
+ parser.add_argument('--device', default='auto', choices=['auto', 'cuda', 'mps', 'cpu'])
328
+ parser.add_argument('--epochs', type=int, default=2)
329
+ parser.add_argument('--micro-batch', type=int, default=2)
330
+ parser.add_argument('--grad-accum', type=int, default=8)
331
+ parser.add_argument('--group-size', type=int, default=4)
332
+ parser.add_argument('--lr-encoder', type=float, default=2.5e-5)
333
+ parser.add_argument('--lr-head', type=float, default=1e-4)
334
+ parser.add_argument('--sigma-start', type=float, default=0.4)
335
+ parser.add_argument('--sigma-end', type=float, default=0.1)
336
+ parser.add_argument('--calib-seed', type=int, default=CALIB_SEED)
337
+ parser.add_argument('--calib-frac', type=float, default=0.1)
338
+ parser.add_argument('--intent-weights', default='{}', help='JSON {intent: multiplier} for loss weighting')
339
+ args = parser.parse_args(argv)
340
+ return train(args)
341
+
342
+
343
+ if __name__ == '__main__':
344
+ sys.exit(main())
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """Train the System One ngram tier scorer (docs/system-one-training.md, docs/system-one-routing.md "N-gram provider").
3
+
4
+ Featurization is stdlib-only and must match harness-everything/scripts/system-one/ngram.js exactly
5
+ (ci/mechanism-system-one-ngram.test.js compares them). torch is imported only inside train() for the optimizer.
6
+ """
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ import math
11
+ from pathlib import Path
12
+ import struct
13
+ import sys
14
+
15
+ sys.dont_write_bytecode = True
16
+ REPO = Path(__file__).resolve().parents[1]
17
+ DEFAULT_HOLDOUT = REPO / 'benchmarks' / 'fixtures' / 'system-one-holdout.json'
18
+ INTENT_HOLDOUT = REPO / 'benchmarks' / 'fixtures' / 'system-one-intent-holdout.json'
19
+
20
+
21
+ def fnv1a32(text):
22
+ h = 0x811c9dc5
23
+ for byte in text.encode('utf-8'):
24
+ h ^= byte
25
+ h = (h * 0x01000193) & 0xFFFFFFFF
26
+ return h
27
+
28
+
29
+ def featurize(text, nmax, dim):
30
+ """bucket -> weight: log(1 + count) per code-point n-gram, one length bucket, L2-normalized."""
31
+ t = ' '.join(text.lower().split())
32
+ counts = {}
33
+ for n in range(1, nmax + 1):
34
+ for i in range(len(t) - n + 1):
35
+ gram = t[i:i + n]
36
+ if not gram.strip():
37
+ continue
38
+ b = fnv1a32(f'{n}:{gram}') % dim
39
+ counts[b] = counts.get(b, 0) + 1
40
+ k = min(8, int(math.floor(math.log2(len(t.encode('utf-8')) + 1))))
41
+ counts[fnv1a32(f'len:{k}') % dim] = 1
42
+ feats = {b: math.log1p(c) for b, c in counts.items()}
43
+ norm = math.sqrt(sum(v * v for v in feats.values()))
44
+ return {b: v / norm for b, v in feats.items()}
45
+
46
+
47
+ def load_jsonl(path):
48
+ return [json.loads(line) for line in Path(path).read_text(encoding='utf-8').splitlines() if line.strip()]
49
+
50
+
51
+ def sha256_file(path):
52
+ return hashlib.sha256(Path(path).read_bytes()).hexdigest()
53
+
54
+
55
+ def train(args):
56
+ import torch
57
+
58
+ torch.manual_seed(args.seed)
59
+ data = Path(args.data_dir)
60
+ intent = args.task == 'intent'
61
+ scored = args.target == 'scores'
62
+ holdout = args.holdout or (INTENT_HOLDOUT if intent else DEFAULT_HOLDOUT)
63
+ corpus = json.loads(Path(holdout).read_text(encoding='utf-8'))
64
+ catalog = [o['id'] for o in corpus['cases'][0]['request']['options']]
65
+ labels_file = data / ('labels-intent-scores.jsonl' if scored else 'labels-intent.jsonl' if intent else 'labels.jsonl')
66
+ if not labels_file.exists():
67
+ sys.exit(f'missing {labels_file.name}')
68
+ overrides = data / ('owner-overrides-intent.jsonl' if intent else 'owner-overrides.jsonl')
69
+ labeled = load_jsonl(labels_file)
70
+ gold = {r['id']: r['gold'] for r in labeled}
71
+ secondary = {r['id']: r.get('secondary', []) for r in labeled} if intent else {}
72
+ relevance = {r['id']: r['scores'] for r in labeled} if scored else {}
73
+ if overrides.exists():
74
+ for r in load_jsonl(overrides):
75
+ gold[r['id']] = r['gold']
76
+ if intent:
77
+ secondary[r['id']] = r.get('secondary', [])
78
+ if scored:
79
+ # An override without scores counts as 0.6 for its primary and 0.4 for its secondary intents.
80
+ relevance[r['id']] = r.get('scores') or {s: 0.6 if s == r['gold'] else 0.4 for s in [r['gold'], *r.get('secondary', [])] if s is not None}
81
+ excluded = data / 'owner-excluded.jsonl'
82
+ skip = {r['id'] for r in load_jsonl(excluded)} if excluded.exists() else set()
83
+ rows = [p for p in load_jsonl(data / 'prompts.jsonl') if p['id'] in gold and p['id'] not in skip]
84
+ index = lambda g: catalog.index('unclassified' if g is None else g)
85
+ split = {name: [r for r in rows if r['split'] == name] for name in ('train', 'validation')}
86
+
87
+ def matrix(items):
88
+ ii, jj, vv = [], [], []
89
+ for r, item in enumerate(items):
90
+ for b, v in featurize(item['text'], args.nmax, args.dim).items():
91
+ ii.append(r); jj.append(b); vv.append(v)
92
+ return torch.sparse_coo_tensor([ii, jj], vv, (len(items), args.dim), dtype=torch.float32).coalesce()
93
+
94
+ x_train, x_val = matrix(split['train']), matrix(split['validation'])
95
+ y_train = torch.tensor([index(gold[r['id']]) for r in split['train']])
96
+ # Intent targets are soft: the primary alone gets 1.0; with secondaries it gets 0.6 and they share 0.4.
97
+ target = y_train
98
+ if scored:
99
+ # Scored intent labels: each intent is its own binary target with its relevance score as the soft value.
100
+ target = torch.tensor([[(0.6 if gold[r['id']] is None else 0.0) if c == 'unclassified' else float(relevance[r['id']].get(c, 0.0))
101
+ for c in catalog] for r in split['train']], dtype=torch.float32)
102
+ elif intent:
103
+ rows_t = []
104
+ for r in split['train']:
105
+ t = [0.0] * len(catalog)
106
+ sec = [s for s in secondary.get(r['id'], []) if s in catalog]
107
+ if gold[r['id']] is None or not sec:
108
+ t[index(gold[r['id']])] = 1.0
109
+ else:
110
+ t[index(gold[r['id']])] = 0.6
111
+ for s in sec:
112
+ t[catalog.index(s)] += 0.4 / len(sec)
113
+ rows_t.append(t)
114
+ target = torch.tensor(rows_t, dtype=torch.float32)
115
+ # Class mass counts the soft targets, so an intent seen only as secondary still carries weight.
116
+ counts = target.sum(0) if intent or scored else torch.bincount(y_train, minlength=len(catalog)).float()
117
+ inverse = torch.where(counts > 0, counts.sum() / (len(catalog) * counts.clamp_min(1e-6)), torch.zeros_like(counts))
118
+ weight = {'none': None, 'sqrt': inverse.sqrt(), 'full': inverse}[args.balance]
119
+ W = torch.zeros(args.dim, len(catalog), requires_grad=True)
120
+ bias = torch.zeros(len(catalog), requires_grad=True)
121
+ optimizer = torch.optim.Adam([W, bias], lr=args.lr)
122
+ for _ in range(args.epochs):
123
+ optimizer.zero_grad()
124
+ logits = torch.sparse.mm(x_train, W) + bias
125
+ if scored:
126
+ per = torch.nn.functional.binary_cross_entropy_with_logits(logits, target, reduction='none')
127
+ fit = (per * (weight if weight is not None else 1.0)).mean()
128
+ else:
129
+ fit = torch.nn.functional.cross_entropy(logits, target, weight=weight)
130
+ loss = fit + args.weight_decay * (W ** 2).sum()
131
+ loss.backward()
132
+ optimizer.step()
133
+ with torch.no_grad():
134
+ # The provider returns the softmax over the logits for either loss (docs/system-one-training.md).
135
+ probs = torch.softmax(torch.sparse.mm(x_val, W) + bias, 1)
136
+ out = Path(args.out)
137
+ out.mkdir(parents=True, exist_ok=True)
138
+ weights = W.detach().contiguous().view(-1).tolist()
139
+ (out / 'harness-routing-v1.bin').write_bytes(struct.pack(f'<{len(weights)}f', *weights))
140
+ datasets = {name: sha256_file(data / name) for name in ('prompts.jsonl', labels_file.name)}
141
+ if overrides.exists():
142
+ datasets[overrides.name] = sha256_file(overrides)
143
+ sidecar = {'format': 'harness-ngram', 'formatVersion': 1, 'dim': args.dim, 'nmax': args.nmax, 'hash': 'fnv1a32', 'catalog': catalog,
144
+ 'bias': [float(v) for v in bias.detach().tolist()],
145
+ 'metadata': {'domain': 'harness-routing-v1', 'task': args.task, 'balance': args.balance, 'weightDecay': args.weight_decay, 'epochs': args.epochs,
146
+ 'seed': args.seed, 'datasets': datasets, 'counts': {k: len(v) for k, v in split.items()},
147
+ **({'target': 'scores', 'loss': 'bce'} if scored else {})}}
148
+ (out / 'harness-routing-v1.json').write_text(json.dumps(sidecar), encoding='utf-8')
149
+ with (out / 'val-scores.jsonl').open('w', encoding='utf-8') as fh:
150
+ for r, p in zip(split['validation'], probs.tolist()):
151
+ row = {'id': r['id'], 'gold': gold[r['id']], 'probs': p}
152
+ if intent:
153
+ row['secondary'] = secondary.get(r['id'], [])
154
+ fh.write(json.dumps(row) + '\n')
155
+ y_val = torch.tensor([index(gold[r['id']]) for r in split['validation']])
156
+ accuracy = (probs.argmax(1) == y_val).float().mean().item() if len(y_val) else 0.0
157
+ print(json.dumps({'counts': sidecar['metadata']['counts'], 'validationAccuracy': accuracy}))
158
+
159
+
160
+ def main(argv=None):
161
+ parser = argparse.ArgumentParser(description=__doc__)
162
+ parser.add_argument('--data-dir', required=True)
163
+ parser.add_argument('--out', required=True)
164
+ parser.add_argument('--task', choices=['tier', 'intent'], default='tier')
165
+ parser.add_argument('--target', choices=['label', 'scores'], default='label', help='scores: per-intent relevance targets from labels-intent-scores.jsonl (intent only)')
166
+ parser.add_argument('--holdout', default=None, help='source of the fixed catalog only (defaults to the task holdout); holdout rows are never trained on')
167
+ parser.add_argument('--dim', type=int, default=1 << 16)
168
+ parser.add_argument('--nmax', type=int, default=3)
169
+ parser.add_argument('--balance', choices=['none', 'sqrt', 'full'], default='sqrt')
170
+ parser.add_argument('--weight-decay', type=float, default=1e-4)
171
+ parser.add_argument('--epochs', type=int, default=60)
172
+ parser.add_argument('--lr', type=float, default=0.01)
173
+ parser.add_argument('--seed', type=int, default=7)
174
+ args = parser.parse_args(argv)
175
+ if args.target == 'scores' and args.task != 'intent':
176
+ parser.error('--target scores needs --task intent')
177
+ train(args)
178
+
179
+
180
+ if __name__ == '__main__':
181
+ main()