lunarlabs-core 0.1.0__tar.gz
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.
- lunarlabs_core-0.1.0/LICENSE +21 -0
- lunarlabs_core-0.1.0/PKG-INFO +151 -0
- lunarlabs_core-0.1.0/README.md +140 -0
- lunarlabs_core-0.1.0/lunarlabs_core/__init__.py +35 -0
- lunarlabs_core-0.1.0/lunarlabs_core/escalation.py +129 -0
- lunarlabs_core-0.1.0/lunarlabs_core/fitness.py +79 -0
- lunarlabs_core-0.1.0/lunarlabs_core/pairs.py +89 -0
- lunarlabs_core-0.1.0/lunarlabs_core/trace.py +103 -0
- lunarlabs_core-0.1.0/lunarlabs_core/verdict.py +59 -0
- lunarlabs_core-0.1.0/lunarlabs_core/verifier.py +60 -0
- lunarlabs_core-0.1.0/lunarlabs_core.egg-info/PKG-INFO +151 -0
- lunarlabs_core-0.1.0/lunarlabs_core.egg-info/SOURCES.txt +20 -0
- lunarlabs_core-0.1.0/lunarlabs_core.egg-info/dependency_links.txt +1 -0
- lunarlabs_core-0.1.0/lunarlabs_core.egg-info/top_level.txt +1 -0
- lunarlabs_core-0.1.0/pyproject.toml +19 -0
- lunarlabs_core-0.1.0/setup.cfg +4 -0
- lunarlabs_core-0.1.0/tests/test_escalation.py +118 -0
- lunarlabs_core-0.1.0/tests/test_fitness.py +43 -0
- lunarlabs_core-0.1.0/tests/test_pairs.py +57 -0
- lunarlabs_core-0.1.0/tests/test_trace.py +79 -0
- lunarlabs_core-0.1.0/tests/test_verdict.py +24 -0
- lunarlabs_core-0.1.0/tests/test_verifier.py +45 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lunar Labs
|
|
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.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lunarlabs-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The product-neutral Lunar Labs spine: capture, deterministic verification, fail-open escalation, training-pair extraction, and the fitness gate.
|
|
5
|
+
Author: Lunar Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# lunarlabs-core
|
|
13
|
+
|
|
14
|
+
**The product-neutral spine shared by Lunar Labs products.**
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
candidate (cheap/local) → verify (deterministic oracle) → pass? ✓ done
|
|
18
|
+
→ fail/abstain? ↑ teacher (frontier) → verify
|
|
19
|
+
↓
|
|
20
|
+
append-only trace capture (both verdicts label-worthy)
|
|
21
|
+
↓
|
|
22
|
+
training-pair extraction (retrieval-task Q→A, NEVER raw transcripts)
|
|
23
|
+
↓
|
|
24
|
+
nightly consolidation → FITNESS GATE (promote only if deflection
|
|
25
|
+
improves without regression) → ship or roll back
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Two products are built on this spine:
|
|
29
|
+
|
|
30
|
+
- **Artemis** — a small-model-first cascade for tool calls. It uses the spine to
|
|
31
|
+
deflect spend from frontier models to on-device models; its four oracle
|
|
32
|
+
families (agreement, provenance, structural, execution) are product-specific
|
|
33
|
+
and stay in Artemis.
|
|
34
|
+
- **Avalon** — an adaptive AI runtime. It uses the same spine for its own
|
|
35
|
+
capture → verify → consolidate → gate loop over runtime behavior. Avalon's
|
|
36
|
+
vision is much larger than this library; `lunarlabs-core` is only the shared
|
|
37
|
+
contracts both products import so the safety semantics (abstention, fail-open,
|
|
38
|
+
gating) are defined — and tested — exactly once.
|
|
39
|
+
|
|
40
|
+
This library is stdlib-only and deliberately boring. What is shared is the
|
|
41
|
+
*contract*; what is measured, drafted, and trained stays in each product.
|
|
42
|
+
|
|
43
|
+
## What the spine defines
|
|
44
|
+
|
|
45
|
+
| module | contract | who consumes it |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `verdict.py` | The three-valued verdict: PASS / FAIL / **ABSTAIN**, expressed as `ok` × `label`. | Both products' oracles. |
|
|
48
|
+
| `verifier.py` | The `Oracle` protocol (`output → Verdict`), `all_of` composition, one trivial example oracle. | Both products plug in their own domain oracles. |
|
|
49
|
+
| `escalation.py` | `verified_cascade`: candidate-first routing, **fail-open** on any internal error, `LUNARLABS_*` kill switch. | Artemis's router; Avalon's runtime decision path. |
|
|
50
|
+
| `trace.py` | `TraceRecord` + append-only JSONL `TraceWriter`, with the dual-label capture gate as the only write path. | Artemis's escalation capture; Avalon's observed-event capture. |
|
|
51
|
+
| `pairs.py` | `TrainingPair`: retrieval-task Q→A with provenance. **Never raw transcripts.** | Both products' nightly consolidation / LoRA pipelines. |
|
|
52
|
+
| `fitness.py` | `fitness_gate`: pure decision over measured metrics — promote only on meaningful deflection gain with no keep-set regression. | Both products' nightly retrain jobs. |
|
|
53
|
+
|
|
54
|
+
## The ABSTAIN third verdict is load-bearing
|
|
55
|
+
|
|
56
|
+
Every oracle answers three things, not two. When a check cannot *mechanically*
|
|
57
|
+
decide, it declines:
|
|
58
|
+
|
|
59
|
+
- **PASS + label** — route on it, and it is strong enough to write training data.
|
|
60
|
+
- **FAIL + label** — real evidence of a wrong answer; the pair may be captured.
|
|
61
|
+
- **ABSTAIN** (`ok=False, label=False`) — escalate (costs money, never ships a
|
|
62
|
+
wrong answer) but the trace writer **refuses the pair**, so it poisons
|
|
63
|
+
nothing. Training on abstentions would teach the model the reference's
|
|
64
|
+
arbitrary surface conventions, which raises future agreement, which inflates
|
|
65
|
+
measured deflection: a self-fulfilling metric.
|
|
66
|
+
|
|
67
|
+
`TraceWriter.record_escalation` enforces both conjuncts — the teacher's pass
|
|
68
|
+
must be label-worthy AND the candidate's failure must be label-worthy — so a
|
|
69
|
+
weak oracle cannot poison the retrain it feeds.
|
|
70
|
+
|
|
71
|
+
## Fail-open, and the kill switch
|
|
72
|
+
|
|
73
|
+
Every spine-internal failure — candidate down, timeout, malformed reply, a bug
|
|
74
|
+
in the verifier itself — routes to the teacher instead of raising. Breaking
|
|
75
|
+
the product loses the user permanently; a missed deflection costs a fraction
|
|
76
|
+
of a cent. Nothing in `verified_cascade` raises past its caller.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export LUNARLABS_DISABLED=1 # every call goes straight to the teacher
|
|
80
|
+
export LUNARLABS_DEFLECT=off # same effect, second switch
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Both are read fresh on every call — flipping the env var takes effect without
|
|
84
|
+
a restart or redeploy.
|
|
85
|
+
|
|
86
|
+
## Training pairs are retrieval-task Q→A — never raw transcripts
|
|
87
|
+
|
|
88
|
+
From the JARVIS research: a raw transcript trains surface imitation (phrasing,
|
|
89
|
+
length, the teacher's tics) and drowns the signal you paid an escalation for.
|
|
90
|
+
A Q→A pair isolates exactly the thing the candidate got wrong: *given THIS
|
|
91
|
+
observed context, the correct answer is THAT.* Every `TrainingPair` therefore
|
|
92
|
+
has `task_type="retrieval"`, a question projected from the trace's context,
|
|
93
|
+
the teacher's verified output as the answer, and full provenance (source trace
|
|
94
|
+
id, candidate failure stage/reasons). A pair without provenance is
|
|
95
|
+
unauditable and should be refused downstream.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from lunarlabs_core import (
|
|
101
|
+
TraceWriter, verified_cascade, non_empty_oracle,
|
|
102
|
+
extract_pairs, write_pairs_jsonl, FitnessMetrics, fitness_gate,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
trace = TraceWriter("traces.jsonl")
|
|
106
|
+
|
|
107
|
+
result = verified_cascade(
|
|
108
|
+
candidate=lambda: small_model.answer(request), # cheap path first
|
|
109
|
+
teacher=lambda: frontier_model.answer(request), # only on verified failure
|
|
110
|
+
verify=my_oracle, # your deterministic oracle
|
|
111
|
+
context={"request_id": "..."}, # opaque to the spine
|
|
112
|
+
trace=trace,
|
|
113
|
+
)
|
|
114
|
+
result.handled_by # "candidate" | "teacher"
|
|
115
|
+
result.failed_open # True if the spine broke and got out of the way
|
|
116
|
+
|
|
117
|
+
# Nightly consolidation:
|
|
118
|
+
pairs = extract_pairs(trace.records())
|
|
119
|
+
write_pairs_jsonl(pairs, "train.jsonl")
|
|
120
|
+
|
|
121
|
+
decision = fitness_gate(
|
|
122
|
+
FitnessMetrics(deflection_rate=0.61, keep_set_score=0.92), # base, measured by you
|
|
123
|
+
FitnessMetrics(deflection_rate=0.68, keep_set_score=0.92), # candidate retrain
|
|
124
|
+
)
|
|
125
|
+
print(decision.summary()) # [PROMOTED] deflection 61% -> 68% (more deflection, no regression)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## What is deliberately NOT here
|
|
129
|
+
|
|
130
|
+
- Artemis's four oracle families, its providers/proxy, pricing, audit and
|
|
131
|
+
dashboard — cost/tool-call-specific, they stay in Artemis.
|
|
132
|
+
- Avalon's runtime, planners, and consolidation wiring.
|
|
133
|
+
- Any trainer. `fitness.py` decides; it does not train. Each product owns its
|
|
134
|
+
LoRA/fine-tune stack and its metric measurement.
|
|
135
|
+
|
|
136
|
+
## Docs
|
|
137
|
+
|
|
138
|
+
- `docs/data-volume-falsifier.sql` — the JARVIS data-volume falsifier: SELECT-only
|
|
139
|
+
per-day counts of Carter-signal (user messages, observed-provenance commitments,
|
|
140
|
+
episodic memories, user facts, eval scorecards) over the last 30 days, to test
|
|
141
|
+
whether the real system produces enough usable training pairs/day to feed the
|
|
142
|
+
nightly trainer (the "~50/day starves it" question). Run with
|
|
143
|
+
`psql "$DATABASE_URL" -f docs/data-volume-falsifier.sql`.
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python -m pytest # stdlib-only package; pytest is the only dev dependency
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
MIT © Lunar Labs
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# lunarlabs-core
|
|
2
|
+
|
|
3
|
+
**The product-neutral spine shared by Lunar Labs products.**
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
candidate (cheap/local) → verify (deterministic oracle) → pass? ✓ done
|
|
7
|
+
→ fail/abstain? ↑ teacher (frontier) → verify
|
|
8
|
+
↓
|
|
9
|
+
append-only trace capture (both verdicts label-worthy)
|
|
10
|
+
↓
|
|
11
|
+
training-pair extraction (retrieval-task Q→A, NEVER raw transcripts)
|
|
12
|
+
↓
|
|
13
|
+
nightly consolidation → FITNESS GATE (promote only if deflection
|
|
14
|
+
improves without regression) → ship or roll back
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Two products are built on this spine:
|
|
18
|
+
|
|
19
|
+
- **Artemis** — a small-model-first cascade for tool calls. It uses the spine to
|
|
20
|
+
deflect spend from frontier models to on-device models; its four oracle
|
|
21
|
+
families (agreement, provenance, structural, execution) are product-specific
|
|
22
|
+
and stay in Artemis.
|
|
23
|
+
- **Avalon** — an adaptive AI runtime. It uses the same spine for its own
|
|
24
|
+
capture → verify → consolidate → gate loop over runtime behavior. Avalon's
|
|
25
|
+
vision is much larger than this library; `lunarlabs-core` is only the shared
|
|
26
|
+
contracts both products import so the safety semantics (abstention, fail-open,
|
|
27
|
+
gating) are defined — and tested — exactly once.
|
|
28
|
+
|
|
29
|
+
This library is stdlib-only and deliberately boring. What is shared is the
|
|
30
|
+
*contract*; what is measured, drafted, and trained stays in each product.
|
|
31
|
+
|
|
32
|
+
## What the spine defines
|
|
33
|
+
|
|
34
|
+
| module | contract | who consumes it |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `verdict.py` | The three-valued verdict: PASS / FAIL / **ABSTAIN**, expressed as `ok` × `label`. | Both products' oracles. |
|
|
37
|
+
| `verifier.py` | The `Oracle` protocol (`output → Verdict`), `all_of` composition, one trivial example oracle. | Both products plug in their own domain oracles. |
|
|
38
|
+
| `escalation.py` | `verified_cascade`: candidate-first routing, **fail-open** on any internal error, `LUNARLABS_*` kill switch. | Artemis's router; Avalon's runtime decision path. |
|
|
39
|
+
| `trace.py` | `TraceRecord` + append-only JSONL `TraceWriter`, with the dual-label capture gate as the only write path. | Artemis's escalation capture; Avalon's observed-event capture. |
|
|
40
|
+
| `pairs.py` | `TrainingPair`: retrieval-task Q→A with provenance. **Never raw transcripts.** | Both products' nightly consolidation / LoRA pipelines. |
|
|
41
|
+
| `fitness.py` | `fitness_gate`: pure decision over measured metrics — promote only on meaningful deflection gain with no keep-set regression. | Both products' nightly retrain jobs. |
|
|
42
|
+
|
|
43
|
+
## The ABSTAIN third verdict is load-bearing
|
|
44
|
+
|
|
45
|
+
Every oracle answers three things, not two. When a check cannot *mechanically*
|
|
46
|
+
decide, it declines:
|
|
47
|
+
|
|
48
|
+
- **PASS + label** — route on it, and it is strong enough to write training data.
|
|
49
|
+
- **FAIL + label** — real evidence of a wrong answer; the pair may be captured.
|
|
50
|
+
- **ABSTAIN** (`ok=False, label=False`) — escalate (costs money, never ships a
|
|
51
|
+
wrong answer) but the trace writer **refuses the pair**, so it poisons
|
|
52
|
+
nothing. Training on abstentions would teach the model the reference's
|
|
53
|
+
arbitrary surface conventions, which raises future agreement, which inflates
|
|
54
|
+
measured deflection: a self-fulfilling metric.
|
|
55
|
+
|
|
56
|
+
`TraceWriter.record_escalation` enforces both conjuncts — the teacher's pass
|
|
57
|
+
must be label-worthy AND the candidate's failure must be label-worthy — so a
|
|
58
|
+
weak oracle cannot poison the retrain it feeds.
|
|
59
|
+
|
|
60
|
+
## Fail-open, and the kill switch
|
|
61
|
+
|
|
62
|
+
Every spine-internal failure — candidate down, timeout, malformed reply, a bug
|
|
63
|
+
in the verifier itself — routes to the teacher instead of raising. Breaking
|
|
64
|
+
the product loses the user permanently; a missed deflection costs a fraction
|
|
65
|
+
of a cent. Nothing in `verified_cascade` raises past its caller.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export LUNARLABS_DISABLED=1 # every call goes straight to the teacher
|
|
69
|
+
export LUNARLABS_DEFLECT=off # same effect, second switch
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Both are read fresh on every call — flipping the env var takes effect without
|
|
73
|
+
a restart or redeploy.
|
|
74
|
+
|
|
75
|
+
## Training pairs are retrieval-task Q→A — never raw transcripts
|
|
76
|
+
|
|
77
|
+
From the JARVIS research: a raw transcript trains surface imitation (phrasing,
|
|
78
|
+
length, the teacher's tics) and drowns the signal you paid an escalation for.
|
|
79
|
+
A Q→A pair isolates exactly the thing the candidate got wrong: *given THIS
|
|
80
|
+
observed context, the correct answer is THAT.* Every `TrainingPair` therefore
|
|
81
|
+
has `task_type="retrieval"`, a question projected from the trace's context,
|
|
82
|
+
the teacher's verified output as the answer, and full provenance (source trace
|
|
83
|
+
id, candidate failure stage/reasons). A pair without provenance is
|
|
84
|
+
unauditable and should be refused downstream.
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from lunarlabs_core import (
|
|
90
|
+
TraceWriter, verified_cascade, non_empty_oracle,
|
|
91
|
+
extract_pairs, write_pairs_jsonl, FitnessMetrics, fitness_gate,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
trace = TraceWriter("traces.jsonl")
|
|
95
|
+
|
|
96
|
+
result = verified_cascade(
|
|
97
|
+
candidate=lambda: small_model.answer(request), # cheap path first
|
|
98
|
+
teacher=lambda: frontier_model.answer(request), # only on verified failure
|
|
99
|
+
verify=my_oracle, # your deterministic oracle
|
|
100
|
+
context={"request_id": "..."}, # opaque to the spine
|
|
101
|
+
trace=trace,
|
|
102
|
+
)
|
|
103
|
+
result.handled_by # "candidate" | "teacher"
|
|
104
|
+
result.failed_open # True if the spine broke and got out of the way
|
|
105
|
+
|
|
106
|
+
# Nightly consolidation:
|
|
107
|
+
pairs = extract_pairs(trace.records())
|
|
108
|
+
write_pairs_jsonl(pairs, "train.jsonl")
|
|
109
|
+
|
|
110
|
+
decision = fitness_gate(
|
|
111
|
+
FitnessMetrics(deflection_rate=0.61, keep_set_score=0.92), # base, measured by you
|
|
112
|
+
FitnessMetrics(deflection_rate=0.68, keep_set_score=0.92), # candidate retrain
|
|
113
|
+
)
|
|
114
|
+
print(decision.summary()) # [PROMOTED] deflection 61% -> 68% (more deflection, no regression)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## What is deliberately NOT here
|
|
118
|
+
|
|
119
|
+
- Artemis's four oracle families, its providers/proxy, pricing, audit and
|
|
120
|
+
dashboard — cost/tool-call-specific, they stay in Artemis.
|
|
121
|
+
- Avalon's runtime, planners, and consolidation wiring.
|
|
122
|
+
- Any trainer. `fitness.py` decides; it does not train. Each product owns its
|
|
123
|
+
LoRA/fine-tune stack and its metric measurement.
|
|
124
|
+
|
|
125
|
+
## Docs
|
|
126
|
+
|
|
127
|
+
- `docs/data-volume-falsifier.sql` — the JARVIS data-volume falsifier: SELECT-only
|
|
128
|
+
per-day counts of Carter-signal (user messages, observed-provenance commitments,
|
|
129
|
+
episodic memories, user facts, eval scorecards) over the last 30 days, to test
|
|
130
|
+
whether the real system produces enough usable training pairs/day to feed the
|
|
131
|
+
nightly trainer (the "~50/day starves it" question). Run with
|
|
132
|
+
`psql "$DATABASE_URL" -f docs/data-volume-falsifier.sql`.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python -m pytest # stdlib-only package; pytest is the only dev dependency
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
MIT © Lunar Labs
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""lunarlabs-core: the product-neutral spine shared by Lunar Labs products.
|
|
2
|
+
|
|
3
|
+
capture -> verify/oracle -> escalate (fail-open) ->
|
|
4
|
+
training-pair extraction (retrieval Q->A) ->
|
|
5
|
+
nightly consolidation -> fitness gate / rollback
|
|
6
|
+
|
|
7
|
+
Products (Artemis, Avalon) plug in their own drafters, oracles, and metrics;
|
|
8
|
+
the contracts live here.
|
|
9
|
+
"""
|
|
10
|
+
from .verdict import Verdict, abstain, failed, passed
|
|
11
|
+
from .verifier import Oracle, all_of, non_empty_oracle
|
|
12
|
+
from .trace import TraceRecord, TraceWriter
|
|
13
|
+
from .escalation import (
|
|
14
|
+
DEFLECT_ENV,
|
|
15
|
+
DISABLED_ENV,
|
|
16
|
+
Drafter,
|
|
17
|
+
EscalationResult,
|
|
18
|
+
deflection_disabled,
|
|
19
|
+
verified_cascade,
|
|
20
|
+
)
|
|
21
|
+
from .fitness import FitnessMetrics, GateDecision, fitness_gate
|
|
22
|
+
from .pairs import TrainingPair, extract_pairs, pair_from_trace, write_pairs_jsonl
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Verdict", "abstain", "failed", "passed",
|
|
28
|
+
"Oracle", "all_of", "non_empty_oracle",
|
|
29
|
+
"TraceRecord", "TraceWriter",
|
|
30
|
+
"Drafter", "EscalationResult", "deflection_disabled", "verified_cascade",
|
|
31
|
+
"DISABLED_ENV", "DEFLECT_ENV",
|
|
32
|
+
"FitnessMetrics", "GateDecision", "fitness_gate",
|
|
33
|
+
"TrainingPair", "extract_pairs", "pair_from_trace", "write_pairs_jsonl",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""The escalation contract: candidate first, teacher only when the check fails.
|
|
2
|
+
|
|
3
|
+
The spine's thesis in one function. The candidate (small, cheap, local,
|
|
4
|
+
private) produces an output. A deterministic verifier checks it. Pass — done.
|
|
5
|
+
Fail or abstain — escalate to the teacher (frontier, strong, expensive),
|
|
6
|
+
verify again, and capture the (candidate-failed, teacher-passed) pair as
|
|
7
|
+
training data. The system relies on the teacher less every night the
|
|
8
|
+
candidate retrains on its own misses.
|
|
9
|
+
|
|
10
|
+
FAIL OPEN. Every spine-internal failure — candidate down, timeout, malformed
|
|
11
|
+
reply, a bug in the verifier itself — routes to the teacher instead of
|
|
12
|
+
raising. Breaking the product loses the user permanently; a missed deflection
|
|
13
|
+
costs a fraction of a cent. That asymmetry is not close, so nothing in this
|
|
14
|
+
module is allowed to raise past `verified_cascade`.
|
|
15
|
+
|
|
16
|
+
KILL SWITCH. Set LUNARLABS_DISABLED=1 (or LUNARLABS_DEFLECT=off) in the
|
|
17
|
+
environment and every call goes straight to the teacher — no code change, no
|
|
18
|
+
redeploy. The env is read fresh on every call, deliberately: flipping the
|
|
19
|
+
variable must take effect without restarting anything.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
from .trace import TraceWriter
|
|
28
|
+
from .verdict import Verdict
|
|
29
|
+
from .verifier import Oracle
|
|
30
|
+
|
|
31
|
+
_TRUE = {"1", "true", "yes", "on"}
|
|
32
|
+
_OFF = {"off", "0", "false", "no"}
|
|
33
|
+
|
|
34
|
+
DISABLED_ENV = "LUNARLABS_DISABLED"
|
|
35
|
+
DEFLECT_ENV = "LUNARLABS_DEFLECT"
|
|
36
|
+
|
|
37
|
+
# Produces an output (text / tool call / plan / anything). Zero-arg so the
|
|
38
|
+
# caller closes over its own request state.
|
|
39
|
+
Drafter = Callable[[], Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def deflection_disabled(env: Optional[Dict[str, str]] = None) -> bool:
|
|
43
|
+
"""The kill switch. Read fresh on every call, deliberately."""
|
|
44
|
+
e = os.environ if env is None else env
|
|
45
|
+
if str(e.get(DISABLED_ENV, "")).strip().lower() in _TRUE:
|
|
46
|
+
return True
|
|
47
|
+
return str(e.get(DEFLECT_ENV, "")).strip().lower() in _OFF
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class EscalationResult:
|
|
52
|
+
output: Any # the chosen output
|
|
53
|
+
handled_by: str # "candidate" | "teacher"
|
|
54
|
+
verified: bool # did the chosen output pass verification?
|
|
55
|
+
escalated: bool
|
|
56
|
+
candidate_verdict: Verdict
|
|
57
|
+
teacher_verdict: Optional[Verdict] = None
|
|
58
|
+
failed_open: bool = False # the spine broke and got out of the way
|
|
59
|
+
failure: str = "" # why, for the operator
|
|
60
|
+
captured: bool = False # did this call write a training pair?
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def verified_cascade(
|
|
64
|
+
candidate: Drafter,
|
|
65
|
+
teacher: Drafter,
|
|
66
|
+
verify: Oracle,
|
|
67
|
+
*,
|
|
68
|
+
context: Optional[Dict[str, Any]] = None,
|
|
69
|
+
trace: Optional[TraceWriter] = None,
|
|
70
|
+
enabled: Optional[bool] = None,
|
|
71
|
+
) -> EscalationResult:
|
|
72
|
+
"""Run the candidate, verify, escalate on failure, capture the fix.
|
|
73
|
+
|
|
74
|
+
`enabled=None` reads the kill switch from the environment; pass a bool to
|
|
75
|
+
decide programmatically (tests, per-request overrides).
|
|
76
|
+
"""
|
|
77
|
+
off = (not enabled) if enabled is not None else deflection_disabled()
|
|
78
|
+
candidate_out: Any = None
|
|
79
|
+
cv = Verdict(False, ["deflection disabled"], "disabled", label=False)
|
|
80
|
+
failure = ""
|
|
81
|
+
|
|
82
|
+
if not off:
|
|
83
|
+
try:
|
|
84
|
+
candidate_out = candidate()
|
|
85
|
+
cv = verify(candidate_out)
|
|
86
|
+
except Exception as exc: # noqa: BLE001 — fail open, never break the caller
|
|
87
|
+
candidate_out, failure = None, f"{type(exc).__name__}: {exc}"
|
|
88
|
+
cv = Verdict(False, [failure], "spine_error", label=False)
|
|
89
|
+
|
|
90
|
+
if cv.ok:
|
|
91
|
+
return EscalationResult(
|
|
92
|
+
output=candidate_out, handled_by="candidate",
|
|
93
|
+
verified=True, escalated=False, candidate_verdict=cv,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
teacher_out = teacher()
|
|
97
|
+
try:
|
|
98
|
+
tv = verify(teacher_out)
|
|
99
|
+
except Exception as exc: # noqa: BLE001 — a broken oracle must not eat the
|
|
100
|
+
# teacher's answer. Hand it back unverified and say why.
|
|
101
|
+
failure = failure or f"{type(exc).__name__}: {exc}"
|
|
102
|
+
tv = Verdict(False, [failure], "spine_error", label=False)
|
|
103
|
+
|
|
104
|
+
# Only capture when the candidate actually produced a wrong answer. A
|
|
105
|
+
# crash is not evidence about the model, and disabled routing produced
|
|
106
|
+
# nothing to learn from. record_escalation enforces both label flags.
|
|
107
|
+
captured = False
|
|
108
|
+
if trace is not None and candidate_out is not None and not failure:
|
|
109
|
+
try:
|
|
110
|
+
captured = trace.record_escalation(
|
|
111
|
+
context or {}, candidate_out, cv, teacher_out, tv,
|
|
112
|
+
) is not None
|
|
113
|
+
except Exception: # noqa: BLE001 — capture is a bonus, never a blocker
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
# A broken oracle must not eat the teacher's answer: when the verifier
|
|
117
|
+
# crashed (spine_error) rather than ruled, hand the teacher's output back
|
|
118
|
+
# unverified. When the teacher genuinely failed verification, prefer the
|
|
119
|
+
# candidate's attempt — both are known-wrong, but the candidate's failure
|
|
120
|
+
# at least carries the verifier's reasons.
|
|
121
|
+
chosen = (teacher_out
|
|
122
|
+
if tv.ok or tv.stage == "spine_error" or candidate_out is None
|
|
123
|
+
else candidate_out)
|
|
124
|
+
return EscalationResult(
|
|
125
|
+
output=chosen, handled_by="teacher",
|
|
126
|
+
verified=tv.ok, escalated=True, candidate_verdict=cv,
|
|
127
|
+
teacher_verdict=tv, failed_open=bool(failure), failure=failure,
|
|
128
|
+
captured=captured,
|
|
129
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""The fitness gate: promote a retrain only if it is measurably better.
|
|
2
|
+
|
|
3
|
+
traces -> train candidate -> FITNESS GATE -> promote or rollback
|
|
4
|
+
|
|
5
|
+
The gate is the safety keystone of the nightly consolidation: a retrained
|
|
6
|
+
candidate is promoted only if it deflects MORE work to the cheap path without
|
|
7
|
+
regressing on a held-out "keep" set. That prevents the classic disaster of
|
|
8
|
+
silently shipping a worse model every night.
|
|
9
|
+
|
|
10
|
+
This module is the CONTRACT, not the wiring. It is a pure function over
|
|
11
|
+
measured metrics — it does not know what a model is, how deflection was
|
|
12
|
+
measured, or what the keep set contains. Each product measures its own
|
|
13
|
+
`FitnessMetrics` (Artemis: cascade replay over eval cases; Avalon: whatever its
|
|
14
|
+
runtime's held-out tasks are) and hands the numbers here. The rule is what is
|
|
15
|
+
shared:
|
|
16
|
+
|
|
17
|
+
1. REGRESSION CHECK FIRST. If the candidate's keep-set score dropped by
|
|
18
|
+
more than `regression_tolerance`, reject — regardless of the gain.
|
|
19
|
+
2. Then require a MEANINGFUL gain: candidate deflection must beat base
|
|
20
|
+
deflection by at least `min_gain`. Equal is not better; retraining has
|
|
21
|
+
a cost and a risk, so the bar is strictly above noise.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class FitnessMetrics:
|
|
31
|
+
"""Measured performance of one model generation, produced by the product."""
|
|
32
|
+
deflection_rate: float # fraction of work handled by the cheap path, 0..1
|
|
33
|
+
keep_set_score: Optional[float] = None # held-out regression score, higher is better;
|
|
34
|
+
# None means "no keep set was run"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class GateDecision:
|
|
39
|
+
promoted: bool
|
|
40
|
+
base_deflection: float
|
|
41
|
+
candidate_deflection: float
|
|
42
|
+
reason: str
|
|
43
|
+
|
|
44
|
+
def summary(self) -> str:
|
|
45
|
+
arrow = f"{self.base_deflection:.0%} -> {self.candidate_deflection:.0%}"
|
|
46
|
+
verb = "PROMOTED" if self.promoted else "REJECTED"
|
|
47
|
+
return f"[{verb}] deflection {arrow} ({self.reason})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fitness_gate(
|
|
51
|
+
base: FitnessMetrics,
|
|
52
|
+
candidate: FitnessMetrics,
|
|
53
|
+
*,
|
|
54
|
+
min_gain: float = 0.01,
|
|
55
|
+
regression_tolerance: float = 0.0,
|
|
56
|
+
) -> GateDecision:
|
|
57
|
+
"""Decide whether `candidate` may replace `base`.
|
|
58
|
+
|
|
59
|
+
`min_gain`: the smallest deflection improvement that justifies a promote.
|
|
60
|
+
`regression_tolerance`: how much keep-set score the candidate may lose
|
|
61
|
+
(use a small positive value if the keep-set metric is noisy; 0.0 is strict).
|
|
62
|
+
"""
|
|
63
|
+
if base.keep_set_score is not None and candidate.keep_set_score is not None:
|
|
64
|
+
if candidate.keep_set_score < base.keep_set_score - regression_tolerance:
|
|
65
|
+
return GateDecision(
|
|
66
|
+
False, base.deflection_rate, candidate.deflection_rate,
|
|
67
|
+
f"regressed on keep set "
|
|
68
|
+
f"{base.keep_set_score:.0%}->{candidate.keep_set_score:.0%}",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if candidate.deflection_rate >= base.deflection_rate + min_gain:
|
|
72
|
+
return GateDecision(
|
|
73
|
+
True, base.deflection_rate, candidate.deflection_rate,
|
|
74
|
+
"more deflection, no regression",
|
|
75
|
+
)
|
|
76
|
+
return GateDecision(
|
|
77
|
+
False, base.deflection_rate, candidate.deflection_rate,
|
|
78
|
+
"no meaningful gain",
|
|
79
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Training-pair extraction: traces become Q->A pairs, never raw transcripts.
|
|
2
|
+
|
|
3
|
+
THE FORMAT RULE, and it is not negotiable: every training pair is stored as a
|
|
4
|
+
RETRIEVAL-TASK question-answer pair — a question whose answering requires
|
|
5
|
+
recalling or deriving a specific fact from what the system observed, answered
|
|
6
|
+
by the verified teacher output. Pairs are NEVER stored as raw transcripts.
|
|
7
|
+
|
|
8
|
+
Why (from the JARVIS research): a raw transcript trains the model to imitate
|
|
9
|
+
the surface of a conversation — phrasing, length, the teacher's tics — and the
|
|
10
|
+
signal you actually paid an escalation for is drowned in tokens that carry no
|
|
11
|
+
information. Worse, transcripts teach continuity that never happened: the
|
|
12
|
+
model learns to complete conversations, not to retrieve facts. A Q->A pair
|
|
13
|
+
isolates exactly the thing the candidate got wrong: "given THIS situation,
|
|
14
|
+
the correct answer is THAT." Retrieval form is what lets the nightly retrain
|
|
15
|
+
move the fitness metric instead of the perplexity metric.
|
|
16
|
+
|
|
17
|
+
Concretely: the QUESTION is built from the trace record's context (the
|
|
18
|
+
situation the candidate faced), and the ANSWER is the teacher's verified
|
|
19
|
+
output. `question_from_context` is the one place a product plugs in how its
|
|
20
|
+
context becomes a question; the default just asks for the correct output given
|
|
21
|
+
the serialized context.
|
|
22
|
+
|
|
23
|
+
Provenance rides along on every pair: which trace it came from, what the
|
|
24
|
+
candidate got wrong, and when. A pair without provenance is unauditable and
|
|
25
|
+
should be refused by any downstream trainer.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import time
|
|
31
|
+
from dataclasses import asdict, dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
34
|
+
|
|
35
|
+
# The default context->question projection. Products with structured context
|
|
36
|
+
# (Artemis: messages + tools; Avalon: observed runtime state) should pass their
|
|
37
|
+
# own `question_from_context` that phrases the retrieval task in their domain.
|
|
38
|
+
def default_question(context: Dict[str, Any]) -> str:
|
|
39
|
+
return (
|
|
40
|
+
"Given the following observed context, produce the correct output.\n\n"
|
|
41
|
+
+ json.dumps(context, sort_keys=True, default=str)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class TrainingPair:
|
|
47
|
+
"""One retrieval-task Q->A pair with full provenance."""
|
|
48
|
+
question: str
|
|
49
|
+
answer: str
|
|
50
|
+
task_type: str = "retrieval" # the format contract; do not set to "transcript"
|
|
51
|
+
source_trace_id: str = ""
|
|
52
|
+
candidate_failure_stage: str = ""
|
|
53
|
+
candidate_failure_reasons: List[str] = field(default_factory=list)
|
|
54
|
+
ts: float = field(default_factory=time.time)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def pair_from_trace(
|
|
58
|
+
record: Dict[str, Any],
|
|
59
|
+
*,
|
|
60
|
+
question_from_context: Optional[Callable[[Dict[str, Any]], str]] = None,
|
|
61
|
+
) -> TrainingPair:
|
|
62
|
+
"""Extract one Q->A pair from a trace record (see trace.TraceRecord)."""
|
|
63
|
+
project = question_from_context or default_question
|
|
64
|
+
answer = record.get("teacher_output")
|
|
65
|
+
return TrainingPair(
|
|
66
|
+
question=project(record.get("context") or {}),
|
|
67
|
+
answer=answer if isinstance(answer, str) else json.dumps(answer, default=str),
|
|
68
|
+
source_trace_id=record.get("record_id", ""),
|
|
69
|
+
candidate_failure_stage=record.get("candidate_failure_stage", ""),
|
|
70
|
+
candidate_failure_reasons=list(record.get("candidate_failure_reasons") or []),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_pairs(
|
|
75
|
+
records: List[Dict[str, Any]],
|
|
76
|
+
*,
|
|
77
|
+
question_from_context: Optional[Callable[[Dict[str, Any]], str]] = None,
|
|
78
|
+
) -> List[TrainingPair]:
|
|
79
|
+
return [pair_from_trace(r, question_from_context=question_from_context)
|
|
80
|
+
for r in records]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def write_pairs_jsonl(pairs: List[TrainingPair], out: Union[str, Path]) -> int:
|
|
84
|
+
"""Export pairs as JSONL for the nightly consolidation. Returns count."""
|
|
85
|
+
out = Path(out)
|
|
86
|
+
with out.open("w") as f:
|
|
87
|
+
for p in pairs:
|
|
88
|
+
f.write(json.dumps(asdict(p), default=str) + "\n")
|
|
89
|
+
return len(pairs)
|