@jossuealcala/madre 0.7.1 → 1.0.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.
- package/CHANGELOG.md +833 -1
- package/README.md +92 -28
- package/SECURITY.md +1 -0
- package/bin/madre.mjs +30 -10
- package/docs/REFERENCE.md +158 -79
- package/docs/SDK.md +23 -19
- package/package.json +1 -1
- package/public/app.js +11599 -4186
- package/public/assets/brand/Geist-latin-ext.woff2 +0 -0
- package/public/assets/brand/Geist-latin.woff2 +0 -0
- package/public/assets/brand/GeistMono-latin-ext.woff2 +0 -0
- package/public/assets/brand/GeistMono-latin.woff2 +0 -0
- package/public/assets/brand/JetBrainsMono-latin-ext.woff2 +0 -0
- package/public/assets/brand/JetBrainsMono-latin.woff2 +0 -0
- package/public/assets/brand/OFL-Geist.txt +93 -0
- package/public/assets/brand/OFL-GeistMono.txt +93 -0
- package/public/assets/brand/OFL-JetBrainsMono.txt +93 -0
- package/public/assets/brand/OFL-Sora.txt +93 -0
- package/public/assets/brand/Sora-latin-ext.woff2 +0 -0
- package/public/assets/brand/Sora-latin.woff2 +0 -0
- package/public/assets/brand/madre-app-180.png +0 -0
- package/public/assets/brand/madre-app-192.png +0 -0
- package/public/assets/brand/madre-app-512.png +0 -0
- package/public/assets/brand/madre-app.svg +1 -0
- package/public/assets/brand/madre-favicon.svg +1 -0
- package/public/assets/news/1.0/alerts.webp +0 -0
- package/public/assets/news/1.0/bar.webp +0 -0
- package/public/assets/news/1.0/finder.webp +0 -0
- package/public/assets/news/1.0/github.webp +0 -0
- package/public/assets/news/1.0/intents.webp +0 -0
- package/public/assets/news/1.0/looks.webp +0 -0
- package/public/assets/news/1.0/modules.webp +0 -0
- package/public/assets/news/1.0/mother.webp +0 -0
- package/public/assets/news/1.0/nostromo.webp +0 -0
- package/public/assets/news/1.0/permissions.webp +0 -0
- package/public/assets/news/1.0/room.webp +0 -0
- package/public/assets/news/1.0/settings.webp +0 -0
- package/public/assets/wallpapers/biznaga.webp +0 -0
- package/public/assets/wallpapers/canela.webp +0 -0
- package/public/assets/wallpapers/jarrito.webp +0 -0
- package/public/assets/wallpapers/magna.webp +0 -0
- package/public/assets/wallpapers/oceano.webp +0 -0
- package/public/assets/wallpapers/paraiso.webp +0 -0
- package/public/assets/wallpapers/quila.webp +0 -0
- package/public/assets/wallpapers/tradicion.webp +0 -0
- package/public/assets/wallpapers/tropical.webp +0 -0
- package/public/assets/wallpapers/urbania.webp +0 -0
- package/public/assets/wallpapers/ventura.webp +0 -0
- package/public/assets/wallpapers/xico.webp +0 -0
- package/public/brands.js +28 -2
- package/public/editor.js +744 -0
- package/public/es.js +2863 -1008
- package/public/i18n.js +1 -1
- package/public/index.html +201 -175
- package/public/inquiry.js +11 -11
- package/public/loader.js +79 -0
- package/public/macroverso.js +1497 -0
- package/public/manifest.webmanifest +14 -0
- package/public/nostromo-engine.js +2222 -0
- package/public/resay.js +24 -0
- package/public/review-ask.js +85 -0
- package/public/salas-mac.css +787 -0
- package/public/salas-mac.html +154 -0
- package/public/salas-mac.js +3264 -0
- package/public/salas.css +85 -0
- package/public/salas.html +47 -0
- package/public/salas.js +200 -0
- package/public/styles.css +6303 -414
- package/public/touch.js +83 -0
- package/public/tour-chapters.js +342 -0
- package/public/tour.js +510 -0
- package/public/troubleshooting.js +159 -140
- package/src/adapters/claude.mjs +4 -3
- package/src/adapters/codex.mjs +49 -8
- package/src/adapters/gemini.mjs +30 -10
- package/src/adapters/madre.mjs +110 -18
- package/src/adapters/process.mjs +42 -2
- package/src/auth-probe.mjs +2 -2
- package/src/bridge/devices.mjs +202 -0
- package/src/bridge/handoff.mjs +45 -0
- package/src/bridge/host.mjs +82 -0
- package/src/bridge/network.mjs +133 -0
- package/src/bridge/policy.mjs +89 -0
- package/src/bridge/qr.mjs +287 -0
- package/src/bridge/server.mjs +269 -0
- package/src/capabilities.mjs +5 -9
- package/src/chats.mjs +59 -22
- package/src/checkpoint.mjs +137 -35
- package/src/choices.mjs +45 -6
- package/src/cli-args.mjs +12 -1
- package/src/commands.mjs +15 -15
- package/src/config.mjs +73 -9
- package/src/conversation-context.mjs +5 -2
- package/src/dataset.mjs +3 -1
- package/src/directives.mjs +0 -5
- package/src/enlaces.mjs +451 -0
- package/src/event-store.mjs +55 -10
- package/src/exam.mjs +5 -5
- package/src/extensions.mjs +0 -2
- package/src/file-ops.mjs +42 -0
- package/src/files.mjs +52 -2
- package/src/github.mjs +407 -0
- package/src/json-file.mjs +63 -0
- package/src/lease.mjs +2 -2
- package/src/lobby-work.mjs +103 -0
- package/src/looks.mjs +55 -0
- package/src/maturity.mjs +3 -3
- package/src/mcp/github-server.mjs +164 -0
- package/src/mcp/image-server.mjs +1 -1
- package/src/mcp/memory-server.mjs +11 -3
- package/src/memory.mjs +38 -12
- package/src/models.mjs +109 -1
- package/src/modules/ahp.mjs +4 -4
- package/src/modules/ash.mjs +1 -1
- package/src/modules/bishop-plan.mjs +207 -0
- package/src/modules/bishop.mjs +645 -0
- package/src/modules/choices.mjs +2 -2
- package/src/modules/correo.mjs +1 -1
- package/src/modules/enlaces.mjs +255 -0
- package/src/modules/git-pulse.mjs +2 -2
- package/src/modules/github.mjs +322 -0
- package/src/modules/image-studio.mjs +3 -3
- package/src/modules/index.mjs +19 -4
- package/src/modules/intents-calibration.mjs +338 -0
- package/src/modules/intents-desk.mjs +481 -0
- package/src/modules/intents-observe.mjs +266 -0
- package/src/modules/intents-policy.mjs +414 -0
- package/src/modules/intents-try.mjs +91 -0
- package/src/modules/intents.mjs +145 -0
- package/src/modules/ollama.mjs +9 -5
- package/src/modules/playwright.mjs +121 -11
- package/src/modules/ripley.mjs +1 -1
- package/src/modules/runs.mjs +34 -0
- package/src/modules/sdk.mjs +10 -4
- package/src/ollama.mjs +25 -1
- package/src/outbound.mjs +19 -17
- package/src/override.mjs +100 -0
- package/src/ports.mjs +23 -1
- package/src/prices.mjs +72 -0
- package/src/privacy.mjs +30 -11
- package/src/quota-sources.mjs +0 -8
- package/src/request-guard.mjs +1 -1
- package/src/review-ask.mjs +111 -0
- package/src/room/archivist.mjs +55 -37
- package/src/room/budget.mjs +17 -5
- package/src/room/control.mjs +88 -8
- package/src/room/economy.mjs +35 -7
- package/src/room/escalation.mjs +2 -0
- package/src/room/guard.mjs +26 -2
- package/src/room/prompt.mjs +32 -13
- package/src/room/runs.mjs +151 -0
- package/src/room/vectors.mjs +19 -4
- package/src/room.mjs +402 -54
- package/src/router.mjs +3 -1
- package/src/runs.mjs +137 -0
- package/src/salas.mjs +313 -0
- package/src/sentinel-errors.mjs +1 -1
- package/src/server-sentinel.mjs +124 -0
- package/src/server.mjs +1578 -167
- package/src/setup.mjs +1 -0
- package/src/tour.mjs +159 -0
- package/src/updates.mjs +48 -8
- package/src/usage-sentinel.mjs +1 -1
- package/src/verdict.mjs +7 -7
- package/src/whats-new.mjs +160 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Intenciones, learned from real use: how each turn the human already paid for went.
|
|
2
|
+
//
|
|
3
|
+
// The calibration (intents-calibration.mjs) asks real questions again, at two rungs, and a judge
|
|
4
|
+
// compares. It is the stronger evidence and it costs turns: with an agent that reads the project
|
|
5
|
+
// on every answer, a quick test is millions of tokens. This file costs none. It reads what the
|
|
6
|
+
// room already wrote — the human's message, the reply, the human's word on it, a failure, a STOP,
|
|
7
|
+
// and what the human did next — and says, per turn, whether the rung it ran at was enough.
|
|
8
|
+
//
|
|
9
|
+
// What counts as "not enough" is what a person does when an answer did not do it: says so (a
|
|
10
|
+
// thumbs-down), asks the same thing again within a few minutes, asks it again at a higher rung, or
|
|
11
|
+
// the turn failed. What counts as "enough" is the opposite: a thumbs-up, or moving on to something
|
|
12
|
+
// else. Everything in between — a turn too recent to know, a STOP nobody followed up, a failure
|
|
13
|
+
// that was the provider's and not the answer's — is not counted either way.
|
|
14
|
+
//
|
|
15
|
+
// Pure functions. Nothing here reads a file, calls a model or writes anything: the desk hands in
|
|
16
|
+
// the ledger events and keeps the result in memory.
|
|
17
|
+
|
|
18
|
+
import { budgetTokens } from '../room/budget.mjs';
|
|
19
|
+
import {
|
|
20
|
+
classifyIntent, contextSignals, effortLadder, evidenceKey, lowestObservedSufficient,
|
|
21
|
+
INTENT_TIER, OBSERVED_HELD_BAR, OBSERVED_MIN_TURNS,
|
|
22
|
+
} from './intents-policy.mjs';
|
|
23
|
+
|
|
24
|
+
export { lowestObservedSufficient, OBSERVED_HELD_BAR, OBSERVED_MIN_TURNS };
|
|
25
|
+
|
|
26
|
+
// How long after an answer the human's next move still says something about it. Past it, silence
|
|
27
|
+
// is moving on; inside it, a turn with nothing after it yet is not decided.
|
|
28
|
+
export const OBSERVE_WINDOW_MS = 15 * 60 * 1000;
|
|
29
|
+
// The same request again: most of the words of the one before, in any order. Short messages are
|
|
30
|
+
// left out, because "sí, dale" twice is two go-aheads and not one request repeated.
|
|
31
|
+
export const NEAR_REPEAT = 0.6;
|
|
32
|
+
export const NEAR_REPEAT_MIN_WORDS = 4;
|
|
33
|
+
|
|
34
|
+
const NEGATIVE = new Set(['never', 'bad']);
|
|
35
|
+
const POSITIVE = new Set(['good', 'preference']);
|
|
36
|
+
// A failure that was the provider's or the machine's, not the answer's: a rate limit says nothing
|
|
37
|
+
// about how hard the agent thought. The calibration skips these too.
|
|
38
|
+
// A crash of the CLI is one of them too: it ended the turn before anything was answered.
|
|
39
|
+
const NOT_THE_ANSWER = /rate.?limit|usage limit|quota|\b429\b|too many requests|overloaded|unavailable|HTTP 5\d\d|server error|not ready|not enabled|too long \(|does not know the model|is not found for|not supported|not logged in|\blog ?in\b|sign in|authenticat|unauthori[sz]ed|api.?key|ENOENT|ECONN|network|exited with code|shutting down|MADRE stopped while|waiting for the project/i;
|
|
40
|
+
// Ledgers written before a STOP carried its own flag say so only in words.
|
|
41
|
+
const STOPPED_WORDS = /stopped by the human|detenido por el humano|STOPALL/i;
|
|
42
|
+
|
|
43
|
+
/* ---------- what the reader keeps ---------- */
|
|
44
|
+
|
|
45
|
+
const OBSERVED_TYPES = new Set(['message.created', 'message.failed', 'message.rated', 'usage.recorded', 'turn.stopped']);
|
|
46
|
+
|
|
47
|
+
// One ledger event cut down to what observing needs, or null. The reader of every conversation in
|
|
48
|
+
// the project keeps these instead of whole events: a reply's text is most of a ledger and none of it
|
|
49
|
+
// is read here.
|
|
50
|
+
export function observable(event, chat = null) {
|
|
51
|
+
if (!event || !OBSERVED_TYPES.has(event.type) || event.ghost) return null;
|
|
52
|
+
const p = event.payload ?? {};
|
|
53
|
+
const base = { type: event.type, sequence: event.sequence, timestamp: event.timestamp, ...(chat ? { chat } : {}) };
|
|
54
|
+
if (event.type === 'message.created') {
|
|
55
|
+
if (p.role === 'user' && p.sender === 'you') return { ...base, payload: { messageId: p.messageId, role: p.role, sender: p.sender, target: p.target, text: String(p.text ?? '').slice(0, 4000), model: p.model ?? null, effort: p.effort ?? null, mode: p.mode } };
|
|
56
|
+
if (p.role === 'assistant' && p.parentMessageId) return { ...base, payload: { messageId: p.messageId, parentMessageId: p.parentMessageId, role: p.role, sender: p.sender } };
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (event.type === 'message.failed') return { ...base, payload: { messageId: p.messageId, target: p.target, error: String(p.error ?? '').slice(0, 300), stopped: p.stopped === true } };
|
|
60
|
+
if (event.type === 'message.rated') return { ...base, payload: { messageId: p.messageId, rating: p.rating } };
|
|
61
|
+
if (event.type === 'usage.recorded') return { ...base, payload: { agent: p.agent, messageId: p.messageId ?? null, budgetTokens: Number.isFinite(p.budgetTokens) ? p.budgetTokens : budgetTokens(p.usage ?? {}) } };
|
|
62
|
+
return { ...base, payload: { messageId: p.messageId, agent: p.agent } };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ---------- one turn ---------- */
|
|
66
|
+
|
|
67
|
+
const fold = (text) => String(text ?? '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
68
|
+
const words = (text) => new Set(fold(text).match(/[\p{L}\p{N}]{2,}/gu) ?? []);
|
|
69
|
+
|
|
70
|
+
// Token Jaccard: the share of words the two messages have in common, out of all the words in them.
|
|
71
|
+
export function similarity(a, b) {
|
|
72
|
+
const one = words(a);
|
|
73
|
+
const two = words(b);
|
|
74
|
+
if (Math.min(one.size, two.size) < NEAR_REPEAT_MIN_WORDS) return 0;
|
|
75
|
+
let shared = 0;
|
|
76
|
+
for (const word of one) if (two.has(word)) shared += 1;
|
|
77
|
+
return shared / (one.size + two.size - shared);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const retrying = (text) => contextSignals(text).some((signal) => signal.id === 'retry');
|
|
81
|
+
const when = (event) => { const at = Date.parse(event?.timestamp ?? ''); return Number.isFinite(at) ? at : null; };
|
|
82
|
+
|
|
83
|
+
// The rung a turn ran at: the one sent with it, or the CLI's own default when the room knows it,
|
|
84
|
+
// or `default` when it does not (Claude never says). `defaults` is { agent: effort } or
|
|
85
|
+
// { agent: { effort, byModel: { model: effort } } }.
|
|
86
|
+
function rungOf(agent, model, effort, defaults = {}) {
|
|
87
|
+
const ladder = effortLadder(agent) ?? [];
|
|
88
|
+
if (ladder.includes(effort)) return effort;
|
|
89
|
+
const known = defaults?.[agent];
|
|
90
|
+
const said = typeof known === 'string' ? known : (model && known?.byModel?.[model]) || known?.effort || null;
|
|
91
|
+
return ladder.includes(said) ? said : 'default';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// One observation per turn the human sent to an agent with an effort ladder, in every conversation
|
|
95
|
+
// handed in (an event's `chat` says which; without one they are all one conversation). `agents`
|
|
96
|
+
// narrows it to those ids; `now` is when "too recent" is measured from.
|
|
97
|
+
export function observeTurns(events = [], { agents = null, defaults = {}, now = Date.now(), windowMs = OBSERVE_WINDOW_MS } = {}) {
|
|
98
|
+
const wanted = agents ? new Set(agents.map((agent) => (typeof agent === 'string' ? agent : agent?.id))) : null;
|
|
99
|
+
const chats = new Map();
|
|
100
|
+
for (const event of events) {
|
|
101
|
+
if (!event || event.ghost) continue;
|
|
102
|
+
const key = event.chat ?? 'main';
|
|
103
|
+
(chats.get(key) ?? chats.set(key, []).get(key)).push(event);
|
|
104
|
+
}
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const [chat, list] of chats) out.push(...observeChat(chat, list, { wanted, defaults, now, windowMs }));
|
|
107
|
+
return out.sort((a, b) => String(a.at).localeCompare(String(b.at)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function observeChat(chat, events, { wanted, defaults, now, windowMs }) {
|
|
111
|
+
const humans = [];
|
|
112
|
+
const replies = new Map();
|
|
113
|
+
const failures = new Map();
|
|
114
|
+
const stops = new Set();
|
|
115
|
+
const ratings = new Map();
|
|
116
|
+
const spent = new Map();
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
const p = event.payload ?? {};
|
|
119
|
+
if (event.type === 'message.created' && p.role === 'user' && p.sender === 'you' && p.mode !== 0) humans.push({ id: p.messageId, agent: p.target, text: String(p.text ?? ''), model: p.model ?? null, effort: p.effort ?? null, at: when(event), timestamp: event.timestamp });
|
|
120
|
+
else if (event.type === 'message.created' && p.role === 'assistant' && p.parentMessageId && !replies.has(p.parentMessageId)) replies.set(p.parentMessageId, { id: p.messageId, at: when(event) });
|
|
121
|
+
else if (event.type === 'message.failed' && p.messageId && !failures.has(p.messageId)) failures.set(p.messageId, { at: when(event), stopped: p.stopped === true, error: String(p.error ?? '') });
|
|
122
|
+
else if (event.type === 'turn.stopped' && p.messageId) stops.add(p.messageId);
|
|
123
|
+
else if (event.type === 'message.rated' && p.messageId) ratings.set(p.messageId, p.rating);
|
|
124
|
+
else if (event.type === 'usage.recorded' && p.messageId) {
|
|
125
|
+
const key = `${p.messageId}|${p.agent}`;
|
|
126
|
+
const tokens = Number.isFinite(p.budgetTokens) ? p.budgetTokens : budgetTokens(p.usage ?? {});
|
|
127
|
+
spent.set(key, (spent.get(key) ?? 0) + tokens);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// What each message asks, read the way the composer reads it: with the one before it to the
|
|
132
|
+
// same agent, so "sí, hazlo" after a proposal is the change it agrees to.
|
|
133
|
+
const previous = new Map();
|
|
134
|
+
for (const human of humans) {
|
|
135
|
+
if (!effortLadder(human.agent)) continue;
|
|
136
|
+
const before = previous.get(human.agent);
|
|
137
|
+
human.intent = classifyIntent(human.text, { previous: before ? { intent: before } : null })?.intent ?? null;
|
|
138
|
+
human.rung = rungOf(human.agent, human.model, human.effort, defaults);
|
|
139
|
+
previous.set(human.agent, human.intent);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const [index, turn] of humans.entries()) {
|
|
144
|
+
const ladder = effortLadder(turn.agent);
|
|
145
|
+
if (!ladder || (wanted && !wanted.has(turn.agent)) || !turn.intent || turn.intent === 'archive' || !INTENT_TIER[turn.intent]) continue;
|
|
146
|
+
const reply = replies.get(turn.id) ?? null;
|
|
147
|
+
const failure = failures.get(turn.id) ?? null;
|
|
148
|
+
const stopped = stops.has(turn.id) || failure?.stopped === true || STOPPED_WORDS.test(failure?.error ?? '');
|
|
149
|
+
const ended = reply?.at ?? failure?.at ?? null;
|
|
150
|
+
const next = humans.slice(index + 1).find((one) => one.agent === turn.agent) ?? null;
|
|
151
|
+
const soon = Boolean(next) && ended !== null && next.at !== null && next.at - ended <= windowMs;
|
|
152
|
+
const higher = soon && ladder.includes(next.rung) && ladder.includes(turn.rung) && ladder.indexOf(next.rung) > ladder.indexOf(turn.rung);
|
|
153
|
+
const again = !soon ? null
|
|
154
|
+
: retrying(next.text) ? 'retry'
|
|
155
|
+
: similarity(turn.text, next.text) >= NEAR_REPEAT ? 'repeat'
|
|
156
|
+
: higher && next.intent === turn.intent ? 'higher'
|
|
157
|
+
: null;
|
|
158
|
+
const rating = reply ? ratings.get(reply.id) : undefined;
|
|
159
|
+
const tokens = spent.has(`${turn.id}|${turn.agent}`) ? spent.get(`${turn.id}|${turn.agent}`) : null;
|
|
160
|
+
|
|
161
|
+
let outcome;
|
|
162
|
+
let why;
|
|
163
|
+
let settles = null;
|
|
164
|
+
if (NEGATIVE.has(rating)) [outcome, why] = ['fell-short', 'rated'];
|
|
165
|
+
else if (POSITIVE.has(rating)) [outcome, why] = ['held', 'rated'];
|
|
166
|
+
// A STOP is a fact about the turn, not about the rung: the human may have stopped it because
|
|
167
|
+
// they changed their mind. Only asking again right after says it was not doing the job.
|
|
168
|
+
else if (stopped) [outcome, why] = again ? ['fell-short', 'stopped'] : ['unknown', 'stopped'];
|
|
169
|
+
else if (failure) [outcome, why] = NOT_THE_ANSWER.test(failure.error) ? ['unknown', 'not-the-answer'] : ['fell-short', 'failed'];
|
|
170
|
+
else if (!reply) [outcome, why] = ['unknown', 'in-flight'];
|
|
171
|
+
else if (again) [outcome, why] = ['fell-short', again];
|
|
172
|
+
else if (soon) [outcome, why] = ['held', 'moved-on'];
|
|
173
|
+
else if (ended !== null && now - ended < windowMs) { [outcome, why] = ['unknown', 'recent']; settles = ended + windowMs; }
|
|
174
|
+
else [outcome, why] = ['held', 'quiet'];
|
|
175
|
+
|
|
176
|
+
out.push({ at: turn.timestamp ?? null, chat, messageId: turn.id, agent: turn.agent, model: turn.model, intent: turn.intent, effort: turn.rung, tokens, outcome, why, ...(settles ? { settles } : {}) });
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ---------- folded ---------- */
|
|
182
|
+
|
|
183
|
+
const median = (list = []) => {
|
|
184
|
+
if (!list.length) return null;
|
|
185
|
+
const sorted = [...list].sort((a, b) => a - b);
|
|
186
|
+
const mid = Math.floor(sorted.length / 2);
|
|
187
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const counted = (one) => one?.outcome === 'held' || one?.outcome === 'fell-short';
|
|
191
|
+
|
|
192
|
+
// Every decided observation, per agent, model and kind of request, per rung: how many, how many
|
|
193
|
+
// held, how many fell short, and the middle turn's tokens (null when no turn at that rung was
|
|
194
|
+
// priced). Keyed like the calibration's evidence, so the policy reads both the same way.
|
|
195
|
+
export function foldObserved(observations = []) {
|
|
196
|
+
const cells = new Map();
|
|
197
|
+
for (const one of observations) {
|
|
198
|
+
if (!counted(one) || !effortLadder(one.agent)) continue;
|
|
199
|
+
const key = evidenceKey(one.agent, one.model, one.intent);
|
|
200
|
+
const cell = cells.get(key) ?? { agent: one.agent, model: one.model ?? null, intent: one.intent, rungs: {} };
|
|
201
|
+
const rung = cell.rungs[one.effort] ?? { n: 0, held: 0, short: 0, spent: [] };
|
|
202
|
+
rung.n += 1;
|
|
203
|
+
if (one.outcome === 'held') rung.held += 1; else rung.short += 1;
|
|
204
|
+
if (Number.isFinite(one.tokens)) rung.spent.push(one.tokens);
|
|
205
|
+
cell.rungs[one.effort] = rung;
|
|
206
|
+
cells.set(key, cell);
|
|
207
|
+
}
|
|
208
|
+
for (const cell of cells.values()) {
|
|
209
|
+
for (const [effort, rung] of Object.entries(cell.rungs)) {
|
|
210
|
+
const { spent, ...rest } = rung;
|
|
211
|
+
cell.rungs[effort] = { ...rest, tokens: median(spent), priced: spent.length };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return cells;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The order the sheet reads kinds of request in: the read-only ones the calibration also knows,
|
|
218
|
+
// then the change, then the short reply.
|
|
219
|
+
export const OBSERVED_INTENTS = ['lookup', 'explain', 'review', 'implement', 'diagnose', 'design', 'chat'];
|
|
220
|
+
|
|
221
|
+
// What real use taught about one agent and model, for the sheet. `tokensSaved` is an estimate:
|
|
222
|
+
// for every decided turn that ran above the lowest rung real use showed to be enough for its kind,
|
|
223
|
+
// what it cost minus what the middle turn at that rung costs. Null when no kind has such a rung
|
|
224
|
+
// with a price; never below zero.
|
|
225
|
+
export function observedSummary(observations = [], { agent, model = null } = {}) {
|
|
226
|
+
const ladder = effortLadder(agent);
|
|
227
|
+
const mine = observations.filter((one) => one.agent === agent && (one.model ?? null) === (model ?? null));
|
|
228
|
+
const decided = mine.filter(counted);
|
|
229
|
+
const cells = foldObserved(decided);
|
|
230
|
+
let saved = 0;
|
|
231
|
+
let computable = false;
|
|
232
|
+
const byIntent = OBSERVED_INTENTS.map((intent) => {
|
|
233
|
+
const cell = cells.get(evidenceKey(agent, model, intent));
|
|
234
|
+
const rungs = Object.values(cell?.rungs ?? {});
|
|
235
|
+
const n = rungs.reduce((sum, rung) => sum + rung.n, 0);
|
|
236
|
+
const held = rungs.reduce((sum, rung) => sum + rung.held, 0);
|
|
237
|
+
const lowest = lowestObservedSufficient(cell, ladder);
|
|
238
|
+
// How close the busiest rung is to the eight turns a reading needs.
|
|
239
|
+
const most = rungs.reduce((top, rung) => Math.max(top, rung.n), 0);
|
|
240
|
+
const price = lowest ? cell.rungs[lowest.effort].tokens : null;
|
|
241
|
+
if (lowest && Number.isFinite(price)) {
|
|
242
|
+
computable = true;
|
|
243
|
+
for (const one of decided) {
|
|
244
|
+
if (one.intent !== intent || !Number.isFinite(one.tokens) || !ladder.includes(one.effort)) continue;
|
|
245
|
+
if (ladder.indexOf(one.effort) > ladder.indexOf(lowest.effort)) saved += one.tokens - price;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
intent,
|
|
250
|
+
rung: lowest?.effort ?? null,
|
|
251
|
+
n,
|
|
252
|
+
heldRate: n ? Number((held / n).toFixed(2)) : null,
|
|
253
|
+
seen: lowest ? { n: lowest.n, held: lowest.held } : null,
|
|
254
|
+
most,
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
turns: decided.length,
|
|
259
|
+
held: decided.filter((one) => one.outcome === 'held').length,
|
|
260
|
+
short: decided.filter((one) => one.outcome === 'fell-short').length,
|
|
261
|
+
pending: mine.length - decided.length,
|
|
262
|
+
byIntent,
|
|
263
|
+
tokensSaved: computable ? Math.max(0, Math.round(saved)) : null,
|
|
264
|
+
minTurns: OBSERVED_MIN_TURNS,
|
|
265
|
+
};
|
|
266
|
+
}
|