Files
Meldwork/desktop/src/workspace/invocation-budget.cjs
T
Ryder Sun 3ce566f152 refactor: split the invocation's session, outputs, budget, prompt and failure handling into modules
The 1500-line invokeLeased still mixed session bookkeeping, workspace output
capture, budget accounting, prompt assembly and failure classification with
the run lifecycle. Each is now a small module with unit tests:

- InvocationSession: one place that resumes, adopts, discards (rebuilds) and
  completes the native session; the three copies of the 'start a fresh
  session' dance, one of them dead code, are gone
- InvocationOutputs: workspace baseline, media import and outcome descriptors
- invocation-budget: usage recording; a payload without a recorded wire size
  no longer fails the turn
- invocation-prompt: the prompt one Agent receives (frozen, direct, group)
- invocation-failure: why an invocation ended (stopped, timed out,
  interrupted, cancelled, failed) and the hidden error metadata
- permission helpers for resumed decisions moved next to the permission policy

invokeLeased is about 1000 lines shorter and reads as the run lifecycle.
Behaviour is unchanged; focused invocation tests and the new module tests
pass.
2026-10-16 10:21:25 +08:00

38 lines
1.7 KiB
JavaScript

const { outboundWirePayloadBytes } = require('../collaboration/outbound-payload.cjs')
// What the Agent was sent: wire bytes are reported, tokens are an estimate.
function recordOutboundUsage(budget, outbound) {
if (!budget) return
const bytes = outboundWirePayloadBytes(outbound)
// A payload that was not built by the transport has no recorded wire size;
// skipping it is better than failing the Agent's turn over bookkeeping.
if (!bytes) return
budget.addUsageBatch([
{ dimension: 'outboundBytes', amount: bytes.length, source: 'reported' },
{ dimension: 'inputTokens', amount: Math.ceil(bytes.length / 4), source: 'estimated' },
])
}
// What the Agent produced: prefer the usage the CLI reported over an estimate.
function recordReplyUsage(budget, { usage, replyText, toolCallCount }) {
if (!budget) return
const reportedOutputTokens = Number(usage?.outputTokens)
const hasReportedOutputTokens = Number.isSafeInteger(reportedOutputTokens)
&& reportedOutputTokens >= 0
const reportedCostMicros = Number(usage?.costMicros)
budget.addUsageBatch([
{
dimension: 'outputTokens',
amount: hasReportedOutputTokens ? reportedOutputTokens : Math.ceil(replyText.length / 4),
source: hasReportedOutputTokens ? 'reported' : 'estimated',
},
{ dimension: 'toolCalls', amount: toolCallCount, source: 'estimated' },
...(Number.isSafeInteger(reportedCostMicros) && reportedCostMicros >= 0
? [{ dimension: 'costMicros', amount: reportedCostMicros, source: 'reported' }]
: []),
{ dimension: 'elapsedMs', value: budget.elapsedValue(), source: 'reported' },
])
}
module.exports = { recordOutboundUsage, recordReplyUsage }