@robzilla1738/agentswarm 0.3.0 → 0.6.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 +51 -11
- package/dist/agent.js +18 -2
- package/dist/cli.js +39 -8
- package/dist/config.js +62 -6
- package/dist/crawltools.js +247 -0
- package/dist/deepseek.js +125 -10
- package/dist/executor.js +993 -144
- package/dist/hub.js +85 -6
- package/dist/journal.js +61 -11
- package/dist/memory.js +84 -0
- package/dist/pdftext.js +211 -0
- package/dist/prompts.js +124 -23
- package/dist/report.js +289 -0
- package/dist/run.js +15 -2
- package/dist/sandbox.js +11 -0
- package/dist/searchcore.js +244 -0
- package/dist/state.js +85 -3
- package/dist/tools.js +392 -25
- package/dist/util.js +85 -0
- package/dist/webtools.js +327 -66
- package/package.json +3 -2
- package/ui/out/404/index.html +1 -1
- package/ui/out/404.html +1 -1
- package/ui/out/_next/static/chunks/532-35122e93f37719b9.js +1 -0
- package/ui/out/_next/static/chunks/677-721ce1c8b7a6a317.js +1 -0
- package/ui/out/_next/static/chunks/app/page-dc9f6744d203e76c.js +1 -0
- package/ui/out/_next/static/chunks/app/run/page-3674e103981703a2.js +1 -0
- package/ui/out/_next/static/chunks/app/settings/page-41a5d8ba43ecfd4a.js +1 -0
- package/ui/out/_next/static/css/d95c2ba395730031.css +3 -0
- package/ui/out/fonts/PlanetKosmos.ttf +0 -0
- package/ui/out/index.html +1 -1
- package/ui/out/index.txt +3 -3
- package/ui/out/run/index.html +1 -1
- package/ui/out/run/index.txt +3 -3
- package/ui/out/settings/index.html +1 -1
- package/ui/out/settings/index.txt +3 -3
- package/ui/out/_next/static/chunks/383-289a866b246b41cc.js +0 -1
- package/ui/out/_next/static/chunks/619-ba102abea3e3d0e4.js +0 -1
- package/ui/out/_next/static/chunks/677-7ab85a6f38c3a235.js +0 -1
- package/ui/out/_next/static/chunks/app/page-0fda5b8e77d90b84.js +0 -1
- package/ui/out/_next/static/chunks/app/run/page-07aab6b1224c3c8c.js +0 -1
- package/ui/out/_next/static/chunks/app/settings/page-528482d468d84cfa.js +0 -1
- package/ui/out/_next/static/css/e2c82b53bf4519e8.css +0 -3
- /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → 7_pihFubDGD40BCy2ynlr}/_buildManifest.js +0 -0
- /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → 7_pihFubDGD40BCy2ynlr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Native search intelligence, ported from the author's SearchKit project
|
|
4
|
+
* (~/Code/searchkit): canonical-URL dedup, source classification, quality
|
|
5
|
+
* ranking, and quotable-passage extraction. Pure string/URL processing —
|
|
6
|
+
* no external services or processes; the engines that feed it live in
|
|
7
|
+
* webtools.ts.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.queryTerms = queryTerms;
|
|
11
|
+
exports.expandQueries = expandQueries;
|
|
12
|
+
exports.reformulate = reformulate;
|
|
13
|
+
exports.looksAcademic = looksAcademic;
|
|
14
|
+
exports.canonicalizeUrl = canonicalizeUrl;
|
|
15
|
+
exports.classifySource = classifySource;
|
|
16
|
+
exports.freshnessBoost = freshnessBoost;
|
|
17
|
+
exports.detectDate = detectDate;
|
|
18
|
+
exports.selectPassages = selectPassages;
|
|
19
|
+
exports.scorePage = scorePage;
|
|
20
|
+
exports.resultQualityScore = resultQualityScore;
|
|
21
|
+
exports.mergeCandidates = mergeCandidates;
|
|
22
|
+
exports.passageBonus = passageBonus;
|
|
23
|
+
exports.rankBonus = rankBonus;
|
|
24
|
+
/** Lowercased alphanumeric query tokens, stopword-ish short tokens dropped. */
|
|
25
|
+
function queryTerms(query) {
|
|
26
|
+
const m = query.toLowerCase().match(/[a-z0-9]+/g) || [];
|
|
27
|
+
return [...new Set(m.filter((t) => t.length > 2))];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate a few complementary query phrasings to widen source coverage in
|
|
31
|
+
* one search: the original, a stopword-stripped keyword core (different
|
|
32
|
+
* recall on most engines), and a docs/guide angle for question-shaped
|
|
33
|
+
* queries. Deterministic and low-noise — capped at `max`.
|
|
34
|
+
*/
|
|
35
|
+
function expandQueries(query, max = 3) {
|
|
36
|
+
const base = query.trim();
|
|
37
|
+
const out = [base];
|
|
38
|
+
const terms = queryTerms(query);
|
|
39
|
+
const core = terms.join(" ");
|
|
40
|
+
if (core && core.length > 4 && core !== base.toLowerCase())
|
|
41
|
+
out.push(core);
|
|
42
|
+
if (/^(how|what|why|when|which|where|who|is|are|can|does|do)\b/i.test(base) && terms.length) {
|
|
43
|
+
out.push(`${core} guide`);
|
|
44
|
+
}
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
return out.map((q) => q.trim()).filter((q) => q && !seen.has(q.toLowerCase()) && seen.add(q.toLowerCase())).slice(0, max);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fallback phrasing when a query returns nothing: strip quotes and search
|
|
50
|
+
* operators down to the top keyword terms. Returns "" when no useful
|
|
51
|
+
* simplification exists.
|
|
52
|
+
*/
|
|
53
|
+
function reformulate(query) {
|
|
54
|
+
const cleaned = query
|
|
55
|
+
.replace(/["'""'']/g, " ")
|
|
56
|
+
.replace(/\b(site|intitle|inurl|filetype):\S+/gi, " ");
|
|
57
|
+
const alt = queryTerms(cleaned).slice(0, 6).join(" ");
|
|
58
|
+
return alt && alt !== query.toLowerCase().trim() ? alt : "";
|
|
59
|
+
}
|
|
60
|
+
/** Queries that smell academic trigger the scholarly engines in deep mode. */
|
|
61
|
+
function looksAcademic(query) {
|
|
62
|
+
return /\b(paper|papers|study|studies|research|arxiv|doi|journal|peer.?review(ed)?|preprint|dataset|benchmark|survey|meta.?analysis|citations?|et al)\b/i.test(query);
|
|
63
|
+
}
|
|
64
|
+
const TRACKING_KEYS = new Set(["fbclid", "gclid", "mc_cid", "mc_eid"]);
|
|
65
|
+
/** Stable canonical form for dedup: strip tracking params, www, trailing slash; sort the query. */
|
|
66
|
+
function canonicalizeUrl(url) {
|
|
67
|
+
let u;
|
|
68
|
+
try {
|
|
69
|
+
u = new URL(url);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return url.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
const pairs = [...u.searchParams.entries()].filter(([k]) => !TRACKING_KEYS.has(k.toLowerCase()) && !k.toLowerCase().startsWith("utm_"));
|
|
75
|
+
pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
76
|
+
const query = pairs.length ? "?" + pairs.map(([k, v]) => `${k}=${v}`).join("&") : "";
|
|
77
|
+
const host = u.hostname.toLowerCase().replace(/^www\./, "");
|
|
78
|
+
let path = u.pathname || "/";
|
|
79
|
+
if (path !== "/")
|
|
80
|
+
path = path.replace(/\/+$/, "");
|
|
81
|
+
return `${u.protocol.toLowerCase()}//${host}${path}${query}`;
|
|
82
|
+
}
|
|
83
|
+
const ACADEMIC_HOSTS = [
|
|
84
|
+
"arxiv.org",
|
|
85
|
+
"doi.org",
|
|
86
|
+
"semanticscholar.org",
|
|
87
|
+
"ncbi.nlm.nih.gov",
|
|
88
|
+
"nature.com",
|
|
89
|
+
"sciencedirect.com",
|
|
90
|
+
"springer.com",
|
|
91
|
+
"link.springer.com",
|
|
92
|
+
"scholar.google.com",
|
|
93
|
+
"acm.org",
|
|
94
|
+
"ieee.org",
|
|
95
|
+
];
|
|
96
|
+
function classifySource(domain) {
|
|
97
|
+
const d = domain.toLowerCase();
|
|
98
|
+
if (d.endsWith(".gov") || d.endsWith(".mil"))
|
|
99
|
+
return "government";
|
|
100
|
+
if (d.endsWith(".edu"))
|
|
101
|
+
return "academic";
|
|
102
|
+
if (ACADEMIC_HOSTS.some((h) => d === h || d.endsWith("." + h)))
|
|
103
|
+
return "academic";
|
|
104
|
+
if (["twitter.com", "x.com", "reddit.com", "facebook.com"].some((s) => d.includes(s)))
|
|
105
|
+
return "social";
|
|
106
|
+
if (d.includes("news") || d.includes("reuters.com") || d.includes("apnews.com") || d.includes("bbc."))
|
|
107
|
+
return "news";
|
|
108
|
+
return "secondary";
|
|
109
|
+
}
|
|
110
|
+
/** Recency boost from an ISO date or bare year: +3 <1y, +2 <2y, +1 <5y, 0 older/undated. */
|
|
111
|
+
function freshnessBoost(date, now = Date.now()) {
|
|
112
|
+
if (!date)
|
|
113
|
+
return 0;
|
|
114
|
+
const m = /^(\d{4})(?:-(\d{1,2})(?:-(\d{1,2}))?)?/.exec(date.trim());
|
|
115
|
+
if (!m)
|
|
116
|
+
return 0;
|
|
117
|
+
const t = Date.UTC(Number(m[1]), m[2] ? Number(m[2]) - 1 : 6, m[3] ? Number(m[3]) : 15);
|
|
118
|
+
const years = (now - t) / 31_557_600_000;
|
|
119
|
+
if (years < 1)
|
|
120
|
+
return 3;
|
|
121
|
+
if (years < 2)
|
|
122
|
+
return 2;
|
|
123
|
+
if (years < 5)
|
|
124
|
+
return 1;
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
/** ISO date if present, else a bare year. */
|
|
128
|
+
function detectDate(text) {
|
|
129
|
+
const iso = /\b(20\d{2}-\d{2}-\d{2})\b/.exec(text);
|
|
130
|
+
if (iso)
|
|
131
|
+
return iso[1];
|
|
132
|
+
const year = /\b(20\d{2})\b/.exec(text);
|
|
133
|
+
return year?.[1];
|
|
134
|
+
}
|
|
135
|
+
const WINDOW_WORDS = 60;
|
|
136
|
+
const STRIDE = 30;
|
|
137
|
+
/**
|
|
138
|
+
* Quotable passages: slide a 60-word window (stride 30) over the text and
|
|
139
|
+
* score each window by the fraction of query terms it contains. Deterministic
|
|
140
|
+
* lexical matching — no embeddings. Falls back to the lead window so a hit
|
|
141
|
+
* always carries something quotable.
|
|
142
|
+
*/
|
|
143
|
+
function selectPassages(text, query, maxPassages = 3) {
|
|
144
|
+
const body = text.trim();
|
|
145
|
+
if (!body)
|
|
146
|
+
return [];
|
|
147
|
+
const terms = queryTerms(query);
|
|
148
|
+
const tokens = [...body.matchAll(/\S+/g)];
|
|
149
|
+
if (!tokens.length)
|
|
150
|
+
return [];
|
|
151
|
+
const windows = [];
|
|
152
|
+
for (let i = 0; i < tokens.length; i += STRIDE) {
|
|
153
|
+
const slice = tokens.slice(i, i + WINDOW_WORDS);
|
|
154
|
+
const start = slice[0].index;
|
|
155
|
+
const last = slice[slice.length - 1];
|
|
156
|
+
const chunk = body.slice(start, last.index + last[0].length);
|
|
157
|
+
windows.push({ text: chunk, score: scoreChunk(chunk, terms) });
|
|
158
|
+
if (i + WINDOW_WORDS >= tokens.length)
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
const scored = windows.filter((w) => w.score > 0).sort((a, b) => b.score - a.score);
|
|
162
|
+
const picked = (scored.length ? scored : windows).slice(0, maxPassages);
|
|
163
|
+
return picked.map((p) => ({ text: p.text, score: Math.round(p.score * 10_000) / 10_000 }));
|
|
164
|
+
}
|
|
165
|
+
function scoreChunk(chunk, terms) {
|
|
166
|
+
if (!terms.length)
|
|
167
|
+
return 0;
|
|
168
|
+
const lowered = chunk.toLowerCase();
|
|
169
|
+
let hits = 0;
|
|
170
|
+
for (const t of terms)
|
|
171
|
+
if (lowered.includes(t))
|
|
172
|
+
hits++;
|
|
173
|
+
return hits / terms.length;
|
|
174
|
+
}
|
|
175
|
+
/** Content-quality score for a fetched page (deep mode re-ranking). */
|
|
176
|
+
function scorePage(page, terms) {
|
|
177
|
+
let score = 0;
|
|
178
|
+
const domain = page.domain.toLowerCase();
|
|
179
|
+
const url = page.url.toLowerCase();
|
|
180
|
+
const title = page.title.toLowerCase();
|
|
181
|
+
const type = classifySource(domain);
|
|
182
|
+
if (type === "primary" || type === "government" || type === "academic")
|
|
183
|
+
score += 5;
|
|
184
|
+
if (domain.includes("docs") || url.includes("docs") || title.includes("documentation"))
|
|
185
|
+
score += 5;
|
|
186
|
+
if (domain === "github.com" || domain === "gitlab.com")
|
|
187
|
+
score += 4;
|
|
188
|
+
if (["pypi.org", "npmjs.com", "rubygems.org"].includes(domain))
|
|
189
|
+
score -= 2;
|
|
190
|
+
score += freshnessBoost(page.date);
|
|
191
|
+
const lowered = page.text.toLowerCase();
|
|
192
|
+
for (const t of terms)
|
|
193
|
+
if (lowered.includes(t))
|
|
194
|
+
score += 1;
|
|
195
|
+
score += Math.min(page.text.length / 4000, 1);
|
|
196
|
+
return score;
|
|
197
|
+
}
|
|
198
|
+
const LOW_VALUE_SNIPPET = ["copy a direct link", "file metadata"];
|
|
199
|
+
/** Pre-fetch quality score for one engine result (snippet-level signals only). */
|
|
200
|
+
function resultQualityScore(c) {
|
|
201
|
+
const url = c.url.toLowerCase();
|
|
202
|
+
const title = c.title.toLowerCase();
|
|
203
|
+
const snippet = c.snippet.toLowerCase();
|
|
204
|
+
let score = Math.max(0, 20 - c.rank);
|
|
205
|
+
if (title.includes("official") || snippet.includes("official"))
|
|
206
|
+
score += 4;
|
|
207
|
+
if (title.includes("documentation") || snippet.includes("documentation") || url.includes("docs"))
|
|
208
|
+
score += 4;
|
|
209
|
+
if (url.includes("github.com") || url.includes("gitlab.com"))
|
|
210
|
+
score += 3;
|
|
211
|
+
if (c.engine === "arxiv" || c.engine === "crossref")
|
|
212
|
+
score += 3;
|
|
213
|
+
score += Math.min(2, freshnessBoost(c.date));
|
|
214
|
+
if (LOW_VALUE_SNIPPET.some((t) => snippet.includes(t)))
|
|
215
|
+
score -= 10;
|
|
216
|
+
return score;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Merge results from several engines: quality-rank, dedupe by canonical URL
|
|
220
|
+
* (first/best occurrence wins), cap at maxResults.
|
|
221
|
+
*/
|
|
222
|
+
function mergeCandidates(candidates, maxResults) {
|
|
223
|
+
const ranked = [...candidates].sort((a, b) => resultQualityScore(b) - resultQualityScore(a));
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const c of ranked) {
|
|
227
|
+
const key = canonicalizeUrl(c.url);
|
|
228
|
+
if (seen.has(key))
|
|
229
|
+
continue;
|
|
230
|
+
seen.add(key);
|
|
231
|
+
out.push(c);
|
|
232
|
+
if (out.length >= maxResults)
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
/** Best-passage bonus used in deep-mode composite scoring. */
|
|
238
|
+
function passageBonus(passages) {
|
|
239
|
+
return passages.length ? passages[0].score * 3 : 0;
|
|
240
|
+
}
|
|
241
|
+
/** Engine-rank decay bonus used in composite scoring. */
|
|
242
|
+
function rankBonus(rank, ceiling) {
|
|
243
|
+
return Math.max(0, ceiling - rank) * 0.2;
|
|
244
|
+
}
|
package/dist/state.js
CHANGED
|
@@ -14,11 +14,15 @@ class RunState {
|
|
|
14
14
|
taskOrder = [];
|
|
15
15
|
agents = new Map();
|
|
16
16
|
notes = [];
|
|
17
|
+
phases = [];
|
|
18
|
+
planExcerpt = "";
|
|
17
19
|
conductorLog = [];
|
|
18
20
|
operatorNotes = [];
|
|
19
21
|
usageByModel = new Map();
|
|
20
22
|
totalUsage = { ...types_1.ZERO_USAGE };
|
|
21
23
|
cost = 0;
|
|
24
|
+
/** Sampled cumulative token spend over time (budget sparkline). */
|
|
25
|
+
budgetSeries = [];
|
|
22
26
|
finalSummary;
|
|
23
27
|
finalReportPath;
|
|
24
28
|
lastSeq = 0;
|
|
@@ -29,10 +33,34 @@ class RunState {
|
|
|
29
33
|
constructor(pricing = {}) {
|
|
30
34
|
this.pricing = pricing;
|
|
31
35
|
}
|
|
36
|
+
/** Sub-states for hierarchical teams, keyed by the owning task id. */
|
|
37
|
+
teams = new Map();
|
|
32
38
|
apply(ev) {
|
|
33
39
|
this.lastSeq = ev.seq;
|
|
34
40
|
this.lastT = ev.t;
|
|
35
41
|
this.updatedAt = ev.t;
|
|
42
|
+
// Team-stamped events reduce into their team's sub-state so a sub-swarm's
|
|
43
|
+
// hundred tasks never pollute the root task list. Usage still rolls up
|
|
44
|
+
// here — the run's budget/cost is one number.
|
|
45
|
+
const teamId = typeof ev.teamId === "string" ? ev.teamId : undefined;
|
|
46
|
+
if (teamId) {
|
|
47
|
+
let team = this.teams.get(teamId);
|
|
48
|
+
if (!team) {
|
|
49
|
+
team = new RunState(this.pricing);
|
|
50
|
+
this.teams.set(teamId, team);
|
|
51
|
+
}
|
|
52
|
+
const { teamId: _omit, ...rest } = ev;
|
|
53
|
+
team.apply(rest);
|
|
54
|
+
if (ev.type === "usage") {
|
|
55
|
+
const u = ev.usage;
|
|
56
|
+
const model = ev.model ?? "unknown";
|
|
57
|
+
this.usageByModel.set(model, (0, types_1.addUsage)(this.usageByModel.get(model) ?? { ...types_1.ZERO_USAGE }, u));
|
|
58
|
+
this.totalUsage = (0, types_1.addUsage)(this.totalUsage, u);
|
|
59
|
+
this.cost += (0, types_1.usageCost)(u, this.pricing[model]);
|
|
60
|
+
this.pushBudgetPoint(ev.t);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
36
64
|
switch (ev.type) {
|
|
37
65
|
case "run.created": {
|
|
38
66
|
this.meta = ev.meta;
|
|
@@ -96,9 +124,23 @@ class RunState {
|
|
|
96
124
|
t.report = ev.report;
|
|
97
125
|
t.reportStatus = ev.status;
|
|
98
126
|
t.artifacts = ev.artifacts ?? t.artifacts;
|
|
127
|
+
if (Array.isArray(ev.keyFacts))
|
|
128
|
+
t.keyFacts = ev.keyFacts;
|
|
129
|
+
if (Array.isArray(ev.openQuestions))
|
|
130
|
+
t.openQuestions = ev.openQuestions;
|
|
131
|
+
if (Array.isArray(ev.filesTouched))
|
|
132
|
+
t.filesTouched = ev.filesTouched;
|
|
133
|
+
if (Array.isArray(ev.sources))
|
|
134
|
+
t.sources = ev.sources;
|
|
99
135
|
}
|
|
100
136
|
break;
|
|
101
137
|
}
|
|
138
|
+
case "task.checkpoint": {
|
|
139
|
+
const t = this.tasks.get(ev.taskId);
|
|
140
|
+
if (t)
|
|
141
|
+
t.lastCheckpoint = ev.summary;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
102
144
|
case "verify.result": {
|
|
103
145
|
const t = this.tasks.get(ev.taskId);
|
|
104
146
|
if (t)
|
|
@@ -146,18 +188,37 @@ class RunState {
|
|
|
146
188
|
}
|
|
147
189
|
break;
|
|
148
190
|
}
|
|
191
|
+
case "plan.updated":
|
|
192
|
+
this.planExcerpt = String(ev.excerpt ?? "");
|
|
193
|
+
break;
|
|
194
|
+
case "phase.set":
|
|
195
|
+
this.phases.push({
|
|
196
|
+
t: ev.t,
|
|
197
|
+
name: String(ev.name ?? ""),
|
|
198
|
+
goal: ev.goal,
|
|
199
|
+
exitCriteria: ev.exit_criteria,
|
|
200
|
+
});
|
|
201
|
+
break;
|
|
149
202
|
case "note.added":
|
|
150
203
|
this.notes.push({
|
|
151
204
|
t: ev.t,
|
|
152
205
|
taskId: ev.taskId,
|
|
153
206
|
agentId: ev.agentId,
|
|
154
207
|
key: ev.key,
|
|
208
|
+
kind: ev.kind,
|
|
155
209
|
text: ev.text,
|
|
210
|
+
url: typeof ev.url === "string" ? ev.url : undefined,
|
|
156
211
|
});
|
|
157
212
|
// Reduced state is held live by the hub and the resume seed — keep
|
|
158
|
-
// only the tail that digests/views actually use.
|
|
159
|
-
|
|
160
|
-
|
|
213
|
+
// only the tail that digests/views actually use. Decisions and
|
|
214
|
+
// conflicts are never dropped: they anchor long-horizon coherence.
|
|
215
|
+
if (this.notes.length > 1000) {
|
|
216
|
+
const keep = (n) => n.kind === "decision" || n.kind === "conflict";
|
|
217
|
+
const pinned = this.notes.filter(keep);
|
|
218
|
+
const rest = this.notes.filter((n) => !keep(n));
|
|
219
|
+
rest.splice(0, rest.length - Math.max(0, 1000 - pinned.length));
|
|
220
|
+
this.notes = [...pinned, ...rest].sort((a, b) => a.t - b.t);
|
|
221
|
+
}
|
|
161
222
|
break;
|
|
162
223
|
case "conductor.say":
|
|
163
224
|
this.conductorLog.push({ t: ev.t, text: ev.text });
|
|
@@ -179,6 +240,7 @@ class RunState {
|
|
|
179
240
|
this.usageByModel.set(model, (0, types_1.addUsage)(this.usageByModel.get(model) ?? { ...types_1.ZERO_USAGE }, u));
|
|
180
241
|
this.totalUsage = (0, types_1.addUsage)(this.totalUsage, u);
|
|
181
242
|
this.cost += (0, types_1.usageCost)(u, this.pricing[model]);
|
|
243
|
+
this.pushBudgetPoint(ev.t);
|
|
182
244
|
break;
|
|
183
245
|
}
|
|
184
246
|
case "run.final":
|
|
@@ -187,6 +249,26 @@ class RunState {
|
|
|
187
249
|
break;
|
|
188
250
|
}
|
|
189
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Sample the cumulative spend: a point per meaningful jump (≥0.5% of the
|
|
254
|
+
* budget cap, or 2k tokens unbounded), halving resolution past 600 points.
|
|
255
|
+
*/
|
|
256
|
+
pushBudgetPoint(t) {
|
|
257
|
+
const tokens = this.totalUsage.promptTokens + this.totalUsage.completionTokens;
|
|
258
|
+
const cap = this.meta?.options?.maxTokens ?? 0;
|
|
259
|
+
const minStep = cap > 0 ? Math.max(2000, cap * 0.005) : 2000;
|
|
260
|
+
const last = this.budgetSeries[this.budgetSeries.length - 1];
|
|
261
|
+
if (last && tokens - last.tokens < minStep) {
|
|
262
|
+
last.t = t;
|
|
263
|
+
last.tokens = tokens;
|
|
264
|
+
last.cost = this.cost;
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.budgetSeries.push({ t, tokens, cost: this.cost });
|
|
268
|
+
if (this.budgetSeries.length > 600) {
|
|
269
|
+
this.budgetSeries = this.budgetSeries.filter((_, i) => i % 2 === 0 || i === this.budgetSeries.length - 1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
190
272
|
taskList() {
|
|
191
273
|
return this.taskOrder.map((id) => this.tasks.get(id)).filter(Boolean);
|
|
192
274
|
}
|