init
This commit is contained in:
commit
0964819cdc
34 files changed
+7912
No files matched your search
+160
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* What one conversation got, and what to do with it.
|
||||
*
|
||||
* Two jobs, both pure:
|
||||
*
|
||||
* 1. **Derive** the small record a tool call leaves in the session log. The
|
||||
* harness persists `output.presentationMeta` verbatim as the `meta` of the
|
||||
* `tool/result` event, so this is the one place a result can state facts
|
||||
* about itself without inventing an event type or a side channel.
|
||||
* 2. **Fold** those records into a running total per conversation. The fold is
|
||||
* the projection unit's transition function, which the framework requires to
|
||||
* be pure and to return the *same reference* for events it does not care
|
||||
* about — an unchanged reference costs nothing downstream, while a fresh
|
||||
* object on every tool call would republish on every step.
|
||||
*
|
||||
* ## Why `sourceBytes` rather than a headline saving
|
||||
*
|
||||
* The honest comparison is between what the caller received and what reading
|
||||
* those same files would have cost. The index already recorded every file's
|
||||
* size when it embedded it, so the second figure costs nothing at query time.
|
||||
*
|
||||
* It is still an **upper bound**, and the numbers are labelled that way
|
||||
* everywhere they surface: a careful agent might have read one of those four
|
||||
* files rather than all four. A figure that flatters the tool is worse than no
|
||||
* figure, because it stops being believed the first time somebody checks.
|
||||
*/
|
||||
|
||||
/** State for a conversation that has never called the tool. */
|
||||
export function emptyUsage() {
|
||||
return {
|
||||
version: 1,
|
||||
searches: 0,
|
||||
indexes: 0,
|
||||
failed: 0,
|
||||
excerpts: 0,
|
||||
returnedBytes: 0,
|
||||
sourceBytes: 0,
|
||||
files: 0,
|
||||
weak: 0,
|
||||
// Bytes of avoided context multiplied by the steps that have re-read them.
|
||||
// Accumulated here rather than derived, because deriving it needs a step
|
||||
// count this state does not own — and the fold sees every step anyway.
|
||||
rereadBytes: 0,
|
||||
lastFocus: '',
|
||||
}
|
||||
}
|
||||
|
||||
/** The context a conversation is carrying that it would not have, in bytes. */
|
||||
function avoidedBytesOf(state) {
|
||||
return Math.max(0, state.sourceBytes - state.returnedBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* The record one call writes into the session log.
|
||||
*
|
||||
* Deliberately tiny: it is appended for every recall call and replayed on every
|
||||
* projection rebuild, so it carries totals and never content.
|
||||
*
|
||||
* @param {{action?: string, query?: string}} args the validated call arguments
|
||||
* @param {Record<string, any>} value the canonical result
|
||||
* @returns {{recall: Record<string, any>}} the persisted metadata
|
||||
*/
|
||||
export function deriveUsage(args, value) {
|
||||
const action = value?.action ?? args?.action ?? 'search'
|
||||
const record = { action }
|
||||
if (value?.ok === false) record.failed = 1
|
||||
if (typeof value?.focus?.path === 'string') record.focus = value.focus.path
|
||||
if (value?.weak === true) record.weak = 1
|
||||
|
||||
if (action === 'search' && Array.isArray(value?.results)) {
|
||||
record.excerpts = value.results.length
|
||||
record.returnedBytes = value.results.reduce(
|
||||
(sum, result) => sum + Buffer.byteLength(String(result?.excerpt ?? ''), 'utf8'),
|
||||
0,
|
||||
)
|
||||
}
|
||||
if (action === 'index' && typeof value?.index?.embedded === 'number') {
|
||||
record.embedded = value.index.embedded
|
||||
}
|
||||
if (typeof value?.measure?.sourceBytes === 'number') {
|
||||
record.sourceBytes = value.measure.sourceBytes
|
||||
record.files = value.measure.files
|
||||
}
|
||||
return { recall: record }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold one session event into the running total.
|
||||
*
|
||||
* @param {ReturnType<typeof emptyUsage>} state state covering all prior events
|
||||
* @param {{type?: string, meta?: any}} event the next committed session event
|
||||
* @returns the next state, or **the same reference** when not this unit's event
|
||||
*/
|
||||
export function foldUsage(state, event) {
|
||||
// Every step re-reads whatever the conversation is already carrying, so each
|
||||
// step adds the avoided-so-far to the compounding total. This is what makes
|
||||
// the figure exact rather than an estimate the caller has to supply.
|
||||
if (event?.type === 'step/start') {
|
||||
const carried = avoidedBytesOf(state)
|
||||
if (carried === 0) return state
|
||||
return { ...state, rereadBytes: state.rereadBytes + carried }
|
||||
}
|
||||
|
||||
const record = event?.type === 'tool/result' ? event.meta?.recall : undefined
|
||||
if (record === undefined || record === null || typeof record !== 'object') return state
|
||||
|
||||
const next = {
|
||||
...state,
|
||||
searches: state.searches + (record.action === 'search' ? 1 : 0),
|
||||
indexes: state.indexes + (record.action === 'index' ? 1 : 0),
|
||||
failed: state.failed + (record.failed === 1 ? 1 : 0),
|
||||
excerpts: state.excerpts + (record.excerpts ?? 0),
|
||||
returnedBytes: state.returnedBytes + (record.returnedBytes ?? 0),
|
||||
sourceBytes: state.sourceBytes + (record.sourceBytes ?? 0),
|
||||
files: state.files + (record.files ?? 0),
|
||||
weak: state.weak + (record.weak === 1 ? 1 : 0),
|
||||
lastFocus: typeof record.focus === 'string' ? record.focus : state.lastFocus,
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* The derived figures a surface shows, computed from state.
|
||||
*
|
||||
* Kept out of the state itself so the persisted fold stays a plain running
|
||||
* total: anything derivable is derived on read, which is what lets the formula
|
||||
* change without invalidating every checkpoint in the cache.
|
||||
*
|
||||
* @param {ReturnType<typeof emptyUsage>} state folded totals
|
||||
* @param {{cacheReadPricePerMillion?: number}} [assumptions]
|
||||
* @returns {Record<string, number|string|boolean>}
|
||||
*/
|
||||
export function usageSummary(state, assumptions = {}) {
|
||||
const { cacheReadPricePerMillion = 0.007 } = assumptions
|
||||
// What a read would have cost, less what the excerpt did cost.
|
||||
const avoidedBytes = avoidedBytesOf(state)
|
||||
// bytes/4 is an approximation of tokens; labelled as one wherever it is shown.
|
||||
const avoidedTokens = Math.round(avoidedBytes / 4)
|
||||
const compression = state.sourceBytes > 0 ? state.sourceBytes / Math.max(1, state.returnedBytes) : 0
|
||||
// The compounding figure: context kept out, re-read once per step since.
|
||||
const rereadTokens = Math.round((state.rereadBytes ?? 0) / 4)
|
||||
const avoidedUsd = ((avoidedTokens + rereadTokens) / 1_000_000) * cacheReadPricePerMillion
|
||||
|
||||
return {
|
||||
searches: state.searches,
|
||||
indexes: state.indexes,
|
||||
failed: state.failed,
|
||||
excerpts: state.excerpts,
|
||||
files: state.files,
|
||||
weak: state.weak,
|
||||
returnedBytes: state.returnedBytes,
|
||||
sourceBytes: state.sourceBytes,
|
||||
avoidedBytes,
|
||||
avoidedTokens,
|
||||
compression: Number(compression.toFixed(2)),
|
||||
rereadTokens,
|
||||
avoidedUsd: Number(avoidedUsd.toFixed(4)),
|
||||
lastFocus: state.lastFocus,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user