changebook 0.7.0 → 0.8.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/README.md +4 -0
- package/dist/agregados.js +496 -0
- package/dist/analyze.js +40 -2
- package/dist/friccionDelBrief.js +102 -0
- package/dist/friction.js +1016 -0
- package/dist/git.js +42 -2
- package/dist/guard.js +184 -2
- package/dist/impact.js +172 -17
- package/dist/index.js +31 -1
- package/dist/respuestas.js +111 -0
- package/dist/supabase.js +38 -2
- package/dist/sync.js +36 -5
- package/dist/toolActionPlan.js +98 -0
- package/dist/toolProjectBrief.js +310 -0
- package/dist/toolUsage.js +128 -0
- package/dist/tools.js +332 -332
- package/dist/usage.js +120 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `atlas_usage`: coste, ahorro y valor de las consultas al atlas.
|
|
3
|
+
*
|
|
4
|
+
* PORTADA DEL HOSPEDADO EL 09/08. Nació allí de una pregunta de Raúl el
|
|
5
|
+
* 2026-07-18: «¿cuánto llevamos gastado / ahorrado?» era incontestable para
|
|
6
|
+
* cualquier agente, porque el dato solo vivía detrás de la sesión web. Las
|
|
7
|
+
* preguntas de dinero son preguntas de fundador; el atlas tiene que
|
|
8
|
+
* responderlas.
|
|
9
|
+
*
|
|
10
|
+
* NIVEL DE CUENTA, NO DE PROYECTO: la facturación es por cuenta, así que esta
|
|
11
|
+
* herramienta NO lleva `project`. Es la única del servidor que no lo lleva, y
|
|
12
|
+
* es a propósito.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
import { recordRead } from "./agregados.js";
|
|
16
|
+
import { RO, errorResult, servedCharsOf, toolResult, } from "./respuestas.js";
|
|
17
|
+
import { fmtUsd, latencyByTool, summarizeUsage, } from "./usage.js";
|
|
18
|
+
/**
|
|
19
|
+
* Tope de filas por consulta de PostgREST. Se pagina hasta agotarlo y se AVISA
|
|
20
|
+
* si se alcanzó: un truncamiento leído como total convierte «has ahorrado $X»
|
|
21
|
+
* en una cifra que nadie puede auditar (AUD-C9). El techo de 20 páginas es un
|
|
22
|
+
* cortafuegos, no un limite de negocio — al alcanzarlo se marca truncado.
|
|
23
|
+
*/
|
|
24
|
+
const PAGINA = 1000;
|
|
25
|
+
const MAX_PAGINAS = 20;
|
|
26
|
+
async function paginar(db, tabla, query, orden) {
|
|
27
|
+
const filas = [];
|
|
28
|
+
for (let i = 0; i < MAX_PAGINAS; i += 1) {
|
|
29
|
+
const page = await db.rest(`${tabla}?${query}&order=${orden}&limit=${PAGINA}&offset=${i * PAGINA}`);
|
|
30
|
+
filas.push(...page);
|
|
31
|
+
if (page.length < PAGINA)
|
|
32
|
+
return { filas, truncado: false };
|
|
33
|
+
}
|
|
34
|
+
return { filas, truncado: true };
|
|
35
|
+
}
|
|
36
|
+
export function registrarUsage(server, db) {
|
|
37
|
+
server.registerTool("atlas_usage", {
|
|
38
|
+
title: "Cost, savings and consultation value",
|
|
39
|
+
description: "Account-level usage summary: what the owner paid for analyses this period, what ChangeBook's optimizations avoided (compression, prompt cache, Batch API, model routing, dedup skips) and how much agents consulted the atlas (with a conservative exploration-avoided estimate). Use when the owner asks about spend, savings, cost or usage. Args: period 'month' (default) or 'all'.",
|
|
40
|
+
inputSchema: { period: z.enum(["month", "all"]).default("month") },
|
|
41
|
+
annotations: RO,
|
|
42
|
+
}, async ({ period }) => {
|
|
43
|
+
const t0 = Date.now();
|
|
44
|
+
try {
|
|
45
|
+
const now = new Date();
|
|
46
|
+
const since = period === "month"
|
|
47
|
+
? new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString()
|
|
48
|
+
: "1970-01-01T00:00:00Z";
|
|
49
|
+
const [ledgerP, skipsP, readsP, rolledUpP] = await Promise.all([
|
|
50
|
+
paginar(db, "usage_ledger", `select=model,action,input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens,diff_chars,raw_diff_chars&created_at=gte.${since}`, "id.asc"),
|
|
51
|
+
paginar(db, "analysis_skips", `select=id&created_at=gte.${since}`, "id.asc").catch(() => ({ filas: [], truncado: false })),
|
|
52
|
+
paginar(db, "atlas_reads", `select=chars_served,source,tool,latency_ms&created_at=gte.${since}`, "id.asc").catch(() => ({ filas: [], truncado: false })),
|
|
53
|
+
// El resumen mensual de lo ya purgado. Sin esto, «all time» empezaría
|
|
54
|
+
// a significar «el último año» en cuanto la purga se llevara el primer
|
|
55
|
+
// mes, sin dejar de llamarse total (QA 2026-07-19). En period=month no
|
|
56
|
+
// aporta nada: el mes en curso nunca está purgado.
|
|
57
|
+
period === "all"
|
|
58
|
+
? paginar(db, "atlas_reads_monthly", "select=reads,chars_served", "month.asc").catch(() => ({ filas: [], truncado: false }))
|
|
59
|
+
: Promise.resolve({ filas: [], truncado: false }),
|
|
60
|
+
]);
|
|
61
|
+
const ledger = ledgerP.filas;
|
|
62
|
+
const reads = readsP.filas;
|
|
63
|
+
const rolledUp = rolledUpP.filas;
|
|
64
|
+
// SI CUALQUIERA SE TRUNCÓ, todo lo de abajo es un SUELO. Se dice en la
|
|
65
|
+
// salida en vez de servir la cifra a secas: quien lee «has ahorrado $X»
|
|
66
|
+
// tiene derecho a saber si eso es X o «al menos X».
|
|
67
|
+
const truncado = ledgerP.truncado ||
|
|
68
|
+
skipsP.truncado ||
|
|
69
|
+
readsP.truncado ||
|
|
70
|
+
rolledUpP.truncado;
|
|
71
|
+
// Los caracteres del guardián quedan FUERA de la estimación de
|
|
72
|
+
// exploración evitada: su contrafáctico es otro (evita un error, no una
|
|
73
|
+
// lectura). El resumen mensual ya viene guardado con esa exclusión.
|
|
74
|
+
const readsChars = reads
|
|
75
|
+
.filter((r) => r.source !== "guard")
|
|
76
|
+
.reduce((a, r) => a + (r.chars_served ?? 0), 0) +
|
|
77
|
+
rolledUp.reduce((a, r) => a + (r.chars_served ?? 0), 0);
|
|
78
|
+
const readsCount = reads.length + rolledUp.reduce((a, r) => a + (r.reads ?? 0), 0);
|
|
79
|
+
const u = summarizeUsage(ledger, skipsP.filas.length, readsCount, readsChars);
|
|
80
|
+
const naive = u.realCost + u.savedTotal;
|
|
81
|
+
const pct = naive > 0 ? Math.round((u.savedTotal / naive) * 100) : 0;
|
|
82
|
+
const lines = [
|
|
83
|
+
`# ChangeBook usage (${period === "month" ? "this month" : "all time"})`,
|
|
84
|
+
"",
|
|
85
|
+
...(truncado
|
|
86
|
+
? [
|
|
87
|
+
`> **These numbers are a FLOOR, not a total.** The usage tables for this period exceeded the read cap, so some rows were not counted. Every figure below is at least this much, possibly more.`,
|
|
88
|
+
"",
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
91
|
+
`- Analyses paid: ${u.analyses} → ${fmtUsd(u.realCost)}`,
|
|
92
|
+
`- Avoided by optimizations: ${fmtUsd(u.savedTotal)} (${pct}% of the naive cost ${fmtUsd(naive)})`,
|
|
93
|
+
` - Dedup skips (${u.skipsCount}): ${fmtUsd(u.saved.skips)}`,
|
|
94
|
+
` - Batch API: ${fmtUsd(u.saved.batch)}`,
|
|
95
|
+
` - Model routing: ${fmtUsd(u.saved.routing)}`,
|
|
96
|
+
` - Prompt cache: ${fmtUsd(u.saved.cache)}`,
|
|
97
|
+
` - Diff compression: ${fmtUsd(u.saved.compression)}`,
|
|
98
|
+
`- Agent consultations: ${u.reads.count} (≈${Math.round(u.reads.chars / 4)} tokens served; estimated exploration avoided ${fmtUsd(u.reads.explorationEstimate)} — conservative estimate, kept OUT of the hard savings above)`,
|
|
99
|
+
];
|
|
100
|
+
const latency = latencyByTool(reads);
|
|
101
|
+
if (latency.length > 0) {
|
|
102
|
+
lines.push(`- Read latency by tool (server-side, ms):`);
|
|
103
|
+
for (const t of latency) {
|
|
104
|
+
lines.push(` - ${t.tool}: p50 ${t.p50_ms} · p95 ${t.p95_ms} (n=${t.n})`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const salida = toolResult(lines.join("\n"), {
|
|
108
|
+
period,
|
|
109
|
+
// Que el consumidor programático también pueda distinguir un total de
|
|
110
|
+
// un suelo: si esto solo saliera en la prosa, cualquier panel que lea
|
|
111
|
+
// el JSON volvería a publicar la cifra truncada como exacta.
|
|
112
|
+
truncated: truncado,
|
|
113
|
+
analyses: u.analyses,
|
|
114
|
+
real_cost_usd: u.realCost,
|
|
115
|
+
saved_usd: u.saved,
|
|
116
|
+
saved_total_usd: u.savedTotal,
|
|
117
|
+
reads: u.reads,
|
|
118
|
+
latency_by_tool: latency,
|
|
119
|
+
});
|
|
120
|
+
recordRead(db, "atlas_usage", "", servedCharsOf(salida), Date.now() - t0);
|
|
121
|
+
return salida;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return errorResult(error);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=toolUsage.js.map
|