@threadcrumbs/common 0.1.2 → 0.1.4
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 +45 -12
- package/lib/claude-transcript.mjs +108 -5
- package/lib/context-breakdown.mjs +149 -0
- package/lib/jsonl-follower.mjs +11 -0
- package/lib/opencode-store.mjs +130 -37
- package/lib/telemetry-fields.mjs +109 -2
- package/lib/tool-kinds.mjs +256 -0
- package/package.json +1 -1
- package/telemetry/HARNESS-NOTES.md +246 -0
- package/telemetry/README.md +137 -11
- package/telemetry/antigravity.mjs +29 -4
- package/telemetry/claude-code.mjs +59 -23
- package/telemetry/codex.mjs +162 -28
- package/telemetry/cursor.mjs +45 -13
- package/telemetry/hermes.mjs +54 -18
package/README.md
CHANGED
|
@@ -39,21 +39,54 @@ repos/@threadcrumbs/
|
|
|
39
39
|
threadnauts/
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Each app depends on
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
Each app depends on a published version, such as `"@threadcrumbs/common": "^0.1.2"`, because
|
|
43
|
+
that's what npm installs for the app's users. Its `pnpm-workspace.yaml` also overrides that with
|
|
44
|
+
`link:../common`, so an edit here shows up in both apps without a release. Clone this repo next to
|
|
45
|
+
an app before running `pnpm install` there. The apps' CI strips the override and uses npm, since
|
|
46
|
+
this repo is private. This package is on npm as
|
|
47
47
|
[`@threadcrumbs/common`](https://www.npmjs.com/package/@threadcrumbs/common).
|
|
48
48
|
|
|
49
|
-
## Publishing
|
|
49
|
+
## Publishing an app, and this package with it
|
|
50
|
+
|
|
51
|
+
From the app's folder, run:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
pnpm auto-publish minor
|
|
55
|
+
```
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
Use `patch`, `minor` or `major` for how far to bump the app. Leave it off to publish a version
|
|
58
|
+
that's already in `package.json` but not on npm yet. Add `--dry-run` to see the plan and every
|
|
59
|
+
problem without changing anything, or `--yes` to skip the final question.
|
|
60
|
+
|
|
61
|
+
Before it changes anything, it checks:
|
|
62
|
+
- that npm is logged in, and Cloudflare too for Threadnauts;
|
|
63
|
+
- that the app, this repo and the app's community repo are on `master`, committed, and not behind
|
|
64
|
+
`origin`;
|
|
65
|
+
- that the community repo's `CHANGELOG.md` has a `## <version>` entry.
|
|
66
|
+
|
|
67
|
+
Then it shows the plan and waits for you to type `yes`. The plan, in order:
|
|
68
|
+
1. **This package, if needed.** It's published first, when anything in its `files` changed since
|
|
69
|
+
the published version's commit (npm records that as `gitHead`). It gets a patch bump, its
|
|
70
|
+
tests run, then it's published, tagged and pushed.
|
|
71
|
+
2. **The app's version.** The app is bumped, and its range on this package raised when this
|
|
72
|
+
package was just published. The lockfile is refreshed and the change committed.
|
|
73
|
+
3. **Test and build.** The app's tests run and it builds once.
|
|
74
|
+
4. **Pro content.** For Threadnauts, `pro-dist/` is checked against `dist/pro-manifest.json` and
|
|
75
|
+
uploaded to the production content bucket through the key-service repo.
|
|
76
|
+
5. **Publish.** The app is published with `--ignore-scripts`, so npm ships the build that was
|
|
77
|
+
just tested and uploaded, rather than building again. npm may ask for your one-time code.
|
|
78
|
+
6. **Push.** The app is tagged `v<version>` and pushed, which deploys its website. The community
|
|
79
|
+
repo is pushed if it has unpushed commits.
|
|
80
|
+
7. **Check.** It waits for npm to list the version, then checks `npx` runs it.
|
|
81
|
+
|
|
82
|
+
If a step fails, it stops there. Run it again with the same arguments and it carries on: a
|
|
83
|
+
version that was bumped but never published is published as it is, not bumped twice. If it stops
|
|
84
|
+
after npm has the new version, it prints the commands that are left to run by hand. The script
|
|
85
|
+
is `tools/release.mjs`. It isn't in `files`, so it never ships.
|
|
86
|
+
|
|
87
|
+
`pnpm auto-publish` always publishes the app as well. To publish only this package, do it by
|
|
88
|
+
hand: raise `version`, commit, run `npm pack --dry-run` to check the file list, tag
|
|
89
|
+
`v<version>`, push, `npm publish`, then `npm view @threadcrumbs/common version`.
|
|
57
90
|
|
|
58
91
|
## Settings it reads
|
|
59
92
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fsp from 'node:fs/promises'
|
|
2
2
|
import { requestCost } from './anthropic-prices.mjs'
|
|
3
|
+
import { emptyFixed, emptySizes, setFixed, sizeCall, sizeCompaction, sizePrompt, sizeReply, sizeRequest, sizeResult, sizeResumed } from './context-breakdown.mjs'
|
|
3
4
|
import { listFiles, parseJsonLine, readNewLines } from './fsutil.mjs'
|
|
4
5
|
import {
|
|
5
6
|
DETAIL_LIMITS,
|
|
@@ -9,19 +10,26 @@ import {
|
|
|
9
10
|
clipBlock,
|
|
10
11
|
diffHunk,
|
|
11
12
|
emptyActivity,
|
|
13
|
+
emptySample,
|
|
12
14
|
endCall,
|
|
13
15
|
fileList,
|
|
14
16
|
firstChange,
|
|
17
|
+
joinedText,
|
|
15
18
|
keepNewest,
|
|
19
|
+
keepText,
|
|
16
20
|
nextPlan,
|
|
21
|
+
openTool,
|
|
17
22
|
patchLines,
|
|
18
23
|
planItems,
|
|
24
|
+
samplePoint,
|
|
19
25
|
settleCalls,
|
|
20
26
|
startCall,
|
|
27
|
+
textList,
|
|
21
28
|
toolFile,
|
|
22
29
|
toolInput,
|
|
23
30
|
toolTarget,
|
|
24
31
|
} from './telemetry-fields.mjs'
|
|
32
|
+
import { mcpServer } from './tool-kinds.mjs'
|
|
25
33
|
|
|
26
34
|
const HEAD_BYTES = 192 * 1024
|
|
27
35
|
const TAIL_BYTES = 64 * 1024
|
|
@@ -64,11 +72,11 @@ export async function readLiveSessions(dir, isAlive = pidAlive) {
|
|
|
64
72
|
return live
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
export function transcriptReader({ skim = false, detail = false } = {}) {
|
|
75
|
+
export function transcriptReader({ skim = false, detail = false, words = false, sizes = false } = {}) {
|
|
68
76
|
const files = new Map()
|
|
69
77
|
|
|
70
78
|
async function advance(entry, file, size) {
|
|
71
|
-
if (!entry.state || size < entry.state.offset) entry.state = emptyState(entry.sidechain, detail)
|
|
79
|
+
if (!entry.state || size < entry.state.offset) entry.state = emptyState(entry.sidechain, detail, words, sizes)
|
|
72
80
|
const state = entry.state
|
|
73
81
|
if (size === state.offset) return state
|
|
74
82
|
try {
|
|
@@ -133,9 +141,12 @@ async function skimInto(state, file, size) {
|
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
function emptyState(sidechain, detail) {
|
|
144
|
+
function emptyState(sidechain, detail, words, sizes) {
|
|
137
145
|
return {
|
|
138
146
|
detail: detail ? emptyDetail() : null,
|
|
147
|
+
words: words ? { thoughts: [], replies: [] } : null,
|
|
148
|
+
sizes: sizes ? emptySizes() : null,
|
|
149
|
+
sent: sizes ? emptySent() : null,
|
|
139
150
|
offset: 0,
|
|
140
151
|
carry: Buffer.alloc(0),
|
|
141
152
|
sidechain,
|
|
@@ -162,10 +173,12 @@ function emptyState(sidechain, detail) {
|
|
|
162
173
|
open: new Map(),
|
|
163
174
|
ending: null,
|
|
164
175
|
handedBackAt: 0,
|
|
176
|
+
turnStartedAt: 0,
|
|
165
177
|
awaiting: new Map(),
|
|
166
178
|
cost: null,
|
|
167
179
|
plan: null,
|
|
168
180
|
prompts: 0,
|
|
181
|
+
contextSample: emptySample(),
|
|
169
182
|
uuids: new Set(),
|
|
170
183
|
}
|
|
171
184
|
}
|
|
@@ -223,6 +236,7 @@ function applyRecord(state, record) {
|
|
|
223
236
|
state.firstBranch ||= state.gitBranch
|
|
224
237
|
}
|
|
225
238
|
if (state.detail && record.type === 'system' && ownRecord(state, record)) applySystem(state.detail, record, at || state.lastAt)
|
|
239
|
+
if (state.sizes && ownRecord(state, record)) applySent(state, record)
|
|
226
240
|
if (!isTurn(state, record)) return
|
|
227
241
|
if (record.type === 'user') applyUser(state, record, at || state.lastAt)
|
|
228
242
|
else applyAssistant(state, record, at || state.lastAt)
|
|
@@ -239,12 +253,88 @@ function applySystem(detail, record, at) {
|
|
|
239
253
|
}
|
|
240
254
|
}
|
|
241
255
|
|
|
256
|
+
const emptySent = () => ({ system: 0, tools: [], deferred: new Set(), instructions: new Map(), skills: new Map(), skillsOther: 0, memory: new Map(), agents: 0 })
|
|
257
|
+
|
|
258
|
+
function applySent(state, record) {
|
|
259
|
+
const sent = state.sent
|
|
260
|
+
const attachment = record.attachment
|
|
261
|
+
if (record.type === 'system' && record.subtype === 'compact_boundary') {
|
|
262
|
+
sizeCompaction(state.sizes)
|
|
263
|
+
state.sent = { ...emptySent(), system: sent.system, tools: sent.tools }
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
if (record.type === 'api-request-shape') {
|
|
267
|
+
const shape = record.shape || {}
|
|
268
|
+
sent.system = (Array.isArray(shape.system) ? shape.system : []).reduce((n, part) => n + text(part?.text).length, 0)
|
|
269
|
+
sent.tools = (Array.isArray(shape.tools) ? shape.tools : []).map((tool) => ({ name: text(tool?.name), chars: JSON.stringify(tool ?? null).length }))
|
|
270
|
+
} else if (attachment?.type === 'deferred_tools_delta') {
|
|
271
|
+
for (const name of [...listOf(attachment.addedNames), ...listOf(attachment.readdedNames)]) sent.deferred.add(text(name))
|
|
272
|
+
for (const name of listOf(attachment.removedNames)) sent.deferred.delete(text(name))
|
|
273
|
+
} else if (attachment?.type === 'mcp_instructions_delta') {
|
|
274
|
+
listOf(attachment.addedNames).forEach((name, i) => sent.instructions.set(text(name), text(listOf(attachment.addedBlocks)[i]).length))
|
|
275
|
+
for (const name of listOf(attachment.removedNames)) sent.instructions.delete(text(name))
|
|
276
|
+
} else if (attachment?.type === 'skill_listing') {
|
|
277
|
+
if (attachment.isInitial !== false) {
|
|
278
|
+
sent.skills = new Map()
|
|
279
|
+
sent.skillsOther = 0
|
|
280
|
+
}
|
|
281
|
+
const names = new Set(listOf(attachment.names).map(text))
|
|
282
|
+
for (const line of text(attachment.content).split('\n')) {
|
|
283
|
+
const name = /^\s*-\s*(\S+?):(?:\s|$)/.exec(line)?.[1]
|
|
284
|
+
if (name && names.has(name)) sent.skills.set(name, (sent.skills.get(name) || 0) + line.length + 1)
|
|
285
|
+
else sent.skillsOther += line.length + 1
|
|
286
|
+
}
|
|
287
|
+
} else if (attachment?.type === 'instructions') {
|
|
288
|
+
for (const file of listOf(attachment.files)) if (text(file?.path)) sent.memory.set(text(file.path), text(file.content).length)
|
|
289
|
+
} else if (attachment?.type === 'agent_listing_delta') {
|
|
290
|
+
sent.agents = (attachment.isInitial ? 0 : sent.agents) + listOf(attachment.addedLines).reduce((n, line) => n + text(line).length + 1, 0)
|
|
291
|
+
} else {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
setFixed(state.sizes, fixedOf(sent))
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const listOf = (value) => (Array.isArray(value) ? value : [])
|
|
298
|
+
|
|
299
|
+
function fixedOf(sent) {
|
|
300
|
+
const fixed = emptyFixed()
|
|
301
|
+
fixed.system = sent.system + sent.agents + sent.skillsOther
|
|
302
|
+
const servers = new Map()
|
|
303
|
+
const server = (name) => {
|
|
304
|
+
if (!servers.has(name)) servers.set(name, { names: new Set(), chars: 0 })
|
|
305
|
+
return servers.get(name)
|
|
306
|
+
}
|
|
307
|
+
const tool = (name, chars) => {
|
|
308
|
+
const owner = mcpServer(name)
|
|
309
|
+
if (!owner) {
|
|
310
|
+
fixed.builtInTools += chars
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
server(owner).names.add(name)
|
|
314
|
+
server(owner).chars += chars
|
|
315
|
+
}
|
|
316
|
+
for (const loaded of sent.tools) tool(loaded.name, loaded.chars)
|
|
317
|
+
for (const name of sent.deferred) tool(name, name.length + 1)
|
|
318
|
+
for (const [name, chars] of sent.instructions) server(name).chars += chars
|
|
319
|
+
fixed.mcp = new Map([...servers].map(([name, s]) => [name, { tools: s.names.size || null, chars: s.chars }]))
|
|
320
|
+
fixed.skills = new Map(sent.skills)
|
|
321
|
+
fixed.memory = new Map(sent.memory)
|
|
322
|
+
return fixed
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resultChars(block) {
|
|
326
|
+
if (typeof block?.content === 'string') return block.content.length
|
|
327
|
+
return listOf(block?.content).reduce((n, part) => n + text(part?.text).length, 0)
|
|
328
|
+
}
|
|
329
|
+
|
|
242
330
|
function applyUser(state, record, at) {
|
|
243
331
|
const content = record.message?.content
|
|
244
332
|
let answered = false
|
|
333
|
+
if (state.sizes && record.isCompactSummary) sizeResumed(state.sizes)
|
|
245
334
|
if (Array.isArray(content)) {
|
|
246
335
|
for (const block of content) {
|
|
247
336
|
if (block?.type !== 'tool_result') continue
|
|
337
|
+
if (state.sizes) sizeResult(state.sizes, block.tool_use_id, resultChars(block))
|
|
248
338
|
state.open.delete(block.tool_use_id)
|
|
249
339
|
answered = true
|
|
250
340
|
const result = record.toolUseResult
|
|
@@ -254,6 +344,7 @@ function applyUser(state, record, at) {
|
|
|
254
344
|
if (!failed) countEdits(state, result, call)
|
|
255
345
|
}
|
|
256
346
|
}
|
|
347
|
+
if (!state.ending || state.ending.handedBack) state.turnStartedAt = at
|
|
257
348
|
state.ending = MID_TURN
|
|
258
349
|
const notice = record.isMeta ? IDLE_NOTICE.exec(firstText(content)) : null
|
|
259
350
|
if (notice) state.awaiting.delete(notice[1])
|
|
@@ -262,6 +353,7 @@ function applyUser(state, record, at) {
|
|
|
262
353
|
if (!prompt || prompt.startsWith('<')) return
|
|
263
354
|
state.open.clear()
|
|
264
355
|
state.prompts++
|
|
356
|
+
if (state.sizes) sizePrompt(state.sizes, prompt.length)
|
|
265
357
|
if (!state.firstPrompt) state.firstPrompt = prompt
|
|
266
358
|
if (state.detail) {
|
|
267
359
|
settleCalls(state.detail.activity)
|
|
@@ -347,9 +439,11 @@ function applyAssistant(state, record, at) {
|
|
|
347
439
|
cacheWrite: number(usage.cache_creation_input_tokens),
|
|
348
440
|
}
|
|
349
441
|
const previous = state.usageById.get(message.id)
|
|
442
|
+
if (state.sizes && !previous) sizeRequest(state.sizes, next.input + next.cacheRead + next.cacheWrite, at)
|
|
350
443
|
for (const key of Object.keys(next)) state.tokens[key] += next[key] - (previous?.[key] || 0)
|
|
351
444
|
state.usageById.set(message.id, next)
|
|
352
445
|
state.context = next.input + next.cacheRead + next.cacheWrite + next.output
|
|
446
|
+
samplePoint(state.contextSample, state.context, { update: Boolean(previous) })
|
|
353
447
|
const cost = requestCost(message.model, usage)
|
|
354
448
|
let added = null
|
|
355
449
|
if (cost != null) {
|
|
@@ -365,11 +459,13 @@ function applyAssistant(state, record, at) {
|
|
|
365
459
|
if (record.effort) state.effort = text(record.effort)
|
|
366
460
|
const content = Array.isArray(message.content) ? message.content : []
|
|
367
461
|
let calling = false
|
|
462
|
+
if (state.sizes) sizeReply(state.sizes, joinedText(content, 'text').length + joinedText(content, 'thinking').length)
|
|
368
463
|
for (const block of content) {
|
|
369
464
|
if (block?.type !== 'tool_use') continue
|
|
370
465
|
calling = true
|
|
466
|
+
if (state.sizes) sizeCall(state.sizes, text(block.id), text(block.name), JSON.stringify(block.input ?? null).length)
|
|
371
467
|
if (!state.open.has(block.id)) {
|
|
372
|
-
state.open.set(block.id,
|
|
468
|
+
state.open.set(block.id, openTool(text(block.name), at, block.input, state.cwd))
|
|
373
469
|
if (state.detail) {
|
|
374
470
|
startCall(state.detail.activity, {
|
|
375
471
|
id: text(block.id),
|
|
@@ -390,6 +486,11 @@ function applyAssistant(state, record, at) {
|
|
|
390
486
|
state.open.clear()
|
|
391
487
|
state.handedBackAt = at
|
|
392
488
|
}
|
|
489
|
+
if (state.words && record.isApiErrorMessage !== true && message.model !== '<synthetic>') {
|
|
490
|
+
const id = text(message.id) || null
|
|
491
|
+
keepText(state.words.thoughts, 'thoughts', { id, at, text: joinedText(content, 'thinking') })
|
|
492
|
+
keepText(state.words.replies, 'replies', { id, at, text: joinedText(content, 'text') })
|
|
493
|
+
}
|
|
393
494
|
if (!state.detail) return
|
|
394
495
|
if (handedBack) settleCalls(state.detail.activity)
|
|
395
496
|
if (record.isApiErrorMessage === true) {
|
|
@@ -415,7 +516,7 @@ function recordUsage(detail, message, next, previous, context, at, cost) {
|
|
|
415
516
|
detail.models.set(model, totals)
|
|
416
517
|
}
|
|
417
518
|
|
|
418
|
-
export function transcriptDetail(state) {
|
|
519
|
+
export function transcriptDetail(state, words = state.words) {
|
|
419
520
|
const detail = state.detail
|
|
420
521
|
if (!detail) return null
|
|
421
522
|
const models = [...detail.models].map(([model, m]) => ({ model, tokens: { ...m.tokens }, costUsd: m.unpriced ? null : m.usd }))
|
|
@@ -428,6 +529,8 @@ export function transcriptDetail(state) {
|
|
|
428
529
|
plan: planItems(state.plan),
|
|
429
530
|
files: fileList(detail.files, state.cwd),
|
|
430
531
|
prompts: detail.prompts.map((p) => ({ ...p })),
|
|
532
|
+
thoughts: textList(words?.thoughts),
|
|
533
|
+
replies: textList(words?.replies),
|
|
431
534
|
errors: detail.errors.map((e) => ({ ...e })),
|
|
432
535
|
}
|
|
433
536
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const CHARS_PER_TOKEN = 4
|
|
2
|
+
|
|
3
|
+
const add = (map, key, n) => map.set(key, (map.get(key) || 0) + n)
|
|
4
|
+
const sum = (values) => [...values].reduce((total, n) => total + n, 0)
|
|
5
|
+
const noPending = () => ({ prompts: 0, replies: 0, tools: new Map() })
|
|
6
|
+
|
|
7
|
+
export function emptyFixed() {
|
|
8
|
+
return { system: 0, builtInTools: 0, mcp: new Map(), skills: new Map(), memory: new Map() }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function emptySizes() {
|
|
12
|
+
return {
|
|
13
|
+
fixed: emptyFixed(),
|
|
14
|
+
first: null,
|
|
15
|
+
firstPrompt: 0,
|
|
16
|
+
resumed: false,
|
|
17
|
+
lastInput: null,
|
|
18
|
+
lastAt: 0,
|
|
19
|
+
compacted: false,
|
|
20
|
+
pending: noPending(),
|
|
21
|
+
weights: { prompts: 0, replies: 0, summary: 0, tools: new Map() },
|
|
22
|
+
callNames: new Map(),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setFixed(sizes, fixed) {
|
|
27
|
+
sizes.fixed = fixed
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function sizePrompt(sizes, chars) {
|
|
31
|
+
if (!(chars > 0)) return
|
|
32
|
+
if (sizes.lastInput == null) sizes.firstPrompt += chars
|
|
33
|
+
sizes.pending.prompts += chars
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sizeReply(sizes, chars) {
|
|
37
|
+
if (chars > 0) sizes.pending.replies += chars
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sizeCall(sizes, id, name, chars) {
|
|
41
|
+
if (id) sizes.callNames.set(id, name)
|
|
42
|
+
if (chars > 0) add(sizes.pending.tools, name, chars)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sizeResult(sizes, id, chars) {
|
|
46
|
+
const name = sizes.callNames.get(id)
|
|
47
|
+
sizes.callNames.delete(id)
|
|
48
|
+
if (name && chars > 0) add(sizes.pending.tools, name, chars)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sizeResumed(sizes) {
|
|
52
|
+
if (sizes.lastInput == null) sizes.resumed = true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function sizeCompaction(sizes) {
|
|
56
|
+
sizes.compacted = true
|
|
57
|
+
sizes.pending = noPending()
|
|
58
|
+
sizes.callNames.clear()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fixedChars = (fixed) =>
|
|
62
|
+
fixed.system + fixed.builtInTools + sum([...fixed.mcp.values()].map((m) => m.chars)) + sum(fixed.skills.values()) + sum(fixed.memory.values())
|
|
63
|
+
|
|
64
|
+
function spread(weights, pending, tokens) {
|
|
65
|
+
const chars = pending.prompts + pending.replies + sum(pending.tools.values())
|
|
66
|
+
if (!chars) {
|
|
67
|
+
weights.replies += tokens
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
weights.prompts += (tokens * pending.prompts) / chars
|
|
71
|
+
weights.replies += (tokens * pending.replies) / chars
|
|
72
|
+
for (const [name, n] of pending.tools) add(weights.tools, name, (tokens * n) / chars)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const counted = (sizes) => (sizes.first && !sizes.resumed ? sizes.first.input - sizes.first.prompt : 0)
|
|
76
|
+
|
|
77
|
+
function ratio(sizes) {
|
|
78
|
+
const fixed = counted(sizes)
|
|
79
|
+
return fixed > 0 && sizes.first.chars ? fixed / sizes.first.chars : 1 / CHARS_PER_TOKEN
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function sizeRequest(sizes, input, at) {
|
|
83
|
+
if (!(input > 0) || input === sizes.lastInput) return
|
|
84
|
+
if (at) sizes.lastAt = at
|
|
85
|
+
if (sizes.lastInput == null) {
|
|
86
|
+
if (!sizes.firstPrompt) sizes.resumed = true
|
|
87
|
+
sizes.first = { input, prompt: Math.round(sizes.firstPrompt / CHARS_PER_TOKEN), chars: fixedChars(sizes.fixed) }
|
|
88
|
+
spread(sizes.weights, sizes.pending, Math.min(input, sizes.first.prompt))
|
|
89
|
+
} else if (sizes.compacted) {
|
|
90
|
+
const fixed = sizes.first.chars ? Math.round(fixedChars(sizes.fixed) * ratio(sizes)) : counted(sizes)
|
|
91
|
+
sizes.weights = { prompts: 0, replies: 0, summary: Math.max(0, input - fixed), tools: new Map() }
|
|
92
|
+
} else if (input > sizes.lastInput) {
|
|
93
|
+
spread(sizes.weights, sizes.pending, input - sizes.lastInput)
|
|
94
|
+
}
|
|
95
|
+
sizes.compacted = false
|
|
96
|
+
sizes.lastInput = input
|
|
97
|
+
sizes.pending = noPending()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function scaled(entries, total) {
|
|
101
|
+
const weight = sum(entries.map((e) => e.weight))
|
|
102
|
+
if (!weight || total <= 0) return []
|
|
103
|
+
return entries.map(({ weight: w, ...rest }) => ({ ...rest, tokens: Math.round((total * w) / weight) })).filter((e) => e.tokens > 0)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function messageItems(weights, tokens) {
|
|
107
|
+
const entries = [
|
|
108
|
+
{ id: 'prompts', weight: weights.prompts },
|
|
109
|
+
{ id: 'replies', weight: weights.replies },
|
|
110
|
+
{ id: 'summary', weight: weights.summary },
|
|
111
|
+
...[...weights.tools].map(([name, weight]) => ({ id: 'tool', name, weight })),
|
|
112
|
+
]
|
|
113
|
+
return scaled(entries, tokens).sort((a, b) => b.tokens - a.tokens)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const twoParts = (base, weights, system) => ({
|
|
117
|
+
...base,
|
|
118
|
+
parts: [
|
|
119
|
+
{ id: 'messages', tokens: base.total - system, items: messageItems(weights, base.total - system) },
|
|
120
|
+
{ id: 'system', tokens: system, items: [] },
|
|
121
|
+
],
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
export function contextBreakdown(sizes, { total, limit = null, reserved = null }) {
|
|
125
|
+
if (!sizes || !(total > 0)) return null
|
|
126
|
+
const base = { at: sizes.lastAt, total, limit: limit || null, reserved }
|
|
127
|
+
const fixed = counted(sizes)
|
|
128
|
+
if (fixed <= 0 || fixed > total) {
|
|
129
|
+
const system = Math.round(fixedChars(sizes.fixed) / CHARS_PER_TOKEN)
|
|
130
|
+
return system > 0 && system < total ? twoParts(base, sizes.weights, system) : null
|
|
131
|
+
}
|
|
132
|
+
if (!sizes.first.chars) return twoParts(base, sizes.weights, fixed)
|
|
133
|
+
const tokens = (n) => Math.round(n * ratio(sizes))
|
|
134
|
+
const { system, builtInTools, mcp, skills, memory } = sizes.fixed
|
|
135
|
+
const servers = [...mcp].map(([name, m]) => ({ name, tools: m.tools ?? null, tokens: tokens(m.chars) })).filter((m) => m.tokens > 0)
|
|
136
|
+
const skillItems = [...skills].map(([name, n]) => ({ name, tokens: tokens(n) })).filter((s) => s.tokens > 0)
|
|
137
|
+
const memoryItems = [...memory].map(([path, n]) => ({ path, tokens: tokens(n) })).filter((m) => m.tokens > 0)
|
|
138
|
+
const parts = [
|
|
139
|
+
{ id: 'system', tokens: tokens(system), items: [] },
|
|
140
|
+
{ id: 'builtInTools', tokens: tokens(builtInTools), items: [] },
|
|
141
|
+
{ id: 'mcpTools', tokens: sum(servers.map((m) => m.tokens)), items: servers.sort((a, b) => b.tokens - a.tokens) },
|
|
142
|
+
{ id: 'skills', tokens: sum(skillItems.map((s) => s.tokens)), items: skillItems.sort((a, b) => b.tokens - a.tokens) },
|
|
143
|
+
{ id: 'memory', tokens: sum(memoryItems.map((m) => m.tokens)), items: memoryItems },
|
|
144
|
+
].filter((p) => p.tokens > 0)
|
|
145
|
+
const fixedNow = sum(parts.map((p) => p.tokens))
|
|
146
|
+
if (fixedNow >= total) return twoParts(base, sizes.weights, fixed)
|
|
147
|
+
const messages = total - fixedNow
|
|
148
|
+
return { ...base, parts: [{ id: 'messages', tokens: messages, items: messageItems(sizes.weights, messages) }, ...parts] }
|
|
149
|
+
}
|
package/lib/jsonl-follower.mjs
CHANGED
|
@@ -43,3 +43,14 @@ export function jsonlFollower({ empty, apply }) {
|
|
|
43
43
|
|
|
44
44
|
return { read, sweep }
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export function recentReaders(make, max) {
|
|
48
|
+
const readers = new Map()
|
|
49
|
+
return (key) => {
|
|
50
|
+
const reader = readers.get(key) || make()
|
|
51
|
+
readers.delete(key)
|
|
52
|
+
readers.set(key, reader)
|
|
53
|
+
while (readers.size > max) readers.delete(readers.keys().next().value)
|
|
54
|
+
return reader
|
|
55
|
+
}
|
|
56
|
+
}
|