67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
/** Combined byte accounting for retained program output. */
|
|
import type { CodeJsonValue, CodeRunFailure, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
|
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
|
|
|
|
/** One run's combined outer-output ledger; binding values never enter it. */
|
|
export class OutputLedger {
|
|
private bytes = 2 // JSON serialization of the empty logs array: []
|
|
private entries = 0
|
|
|
|
constructor(private readonly maxBytes: number) {}
|
|
|
|
/** Admit one exact log entry, or report that the hard cap was crossed. */
|
|
admit(text: string, sink: string[]): boolean {
|
|
const separatorBytes = this.entries > 0 ? 1 : 0
|
|
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes)
|
|
if (stringBytes === undefined) return false
|
|
this.bytes += stringBytes + separatorBytes
|
|
this.entries += 1
|
|
sink.push(text)
|
|
return true
|
|
}
|
|
|
|
/** Finalize a successful absent-or-JSON completion against the combined cap. */
|
|
success(logs: string[], value?: CodeJsonValue): CodeRunResult {
|
|
if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
|
|
return { logs, ...value !== undefined ? { value } : {} }
|
|
}
|
|
|
|
/** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
|
|
failure(logs: string[], error: CodeRunFailure): CodeRunResult {
|
|
if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
|
|
return { logs, error }
|
|
}
|
|
|
|
/** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
|
|
limit(logs: string[]): CodeRunResult {
|
|
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`
|
|
// The fixed diagnostic is ASCII, so every character is one byte plus the quotes.
|
|
const messageBytes = fullMessage.length + 2
|
|
const retained: string[] = []
|
|
let retainedBytes = 2
|
|
const logBudget = this.maxBytes - messageBytes
|
|
for (const text of logs) {
|
|
const separatorBytes = retained.length > 0 ? 1 : 0
|
|
const availableBytes = logBudget - retainedBytes - separatorBytes
|
|
const stringBytes = jsonStringBytesUpTo(text, availableBytes)
|
|
if (stringBytes !== undefined) {
|
|
retained.push(text)
|
|
retainedBytes += stringBytes + separatorBytes
|
|
continue
|
|
}
|
|
const prefix = truncateJsonStringBytes(text, availableBytes)
|
|
if (prefix.length > 0) {
|
|
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
|
|
/* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
|
|
if (prefixBytes === undefined) throw new Error('output ledger produced an oversized log prefix')
|
|
retained.push(prefix)
|
|
retainedBytes += prefixBytes + separatorBytes
|
|
}
|
|
break
|
|
}
|
|
const availableMessageBytes = this.maxBytes - retainedBytes
|
|
const message = truncateJsonStringBytes(fullMessage, availableMessageBytes)
|
|
return { logs: retained, error: { kind: 'output-limit', message } }
|
|
}
|
|
}
|