231 lines
7.3 KiB
JavaScript
231 lines
7.3 KiB
JavaScript
import test from 'brittle'
|
|
import { readFileSync } from 'node:fs'
|
|
import vm from 'node:vm'
|
|
|
|
const STATE_SRC = readFileSync(new URL('../lib/agent/agent-state.js', import.meta.url), 'utf8')
|
|
const COMPACT_SRC = readFileSync(
|
|
new URL('../lib/agent/agent-compact.js', import.meta.url),
|
|
'utf8'
|
|
)
|
|
|
|
function load() {
|
|
const sandbox = { console, TextEncoder, TextDecoder }
|
|
vm.createContext(sandbox)
|
|
vm.runInContext(STATE_SRC, sandbox)
|
|
vm.runInContext(COMPACT_SRC, sandbox)
|
|
return sandbox
|
|
}
|
|
|
|
test('trim for ctx shrinks system when tools consume budget', async (t) => {
|
|
const { bareAgentTrimMessagesForCtx, bareAgentEstimateTokens } = load()
|
|
const tools = [{ name: 'x', description: 'y'.repeat(8000), parameters: {} }]
|
|
const msgs = [
|
|
{ role: 'system', content: 'S'.repeat(20_000) },
|
|
{ role: 'user', content: 'hello' }
|
|
]
|
|
const out = bareAgentTrimMessagesForCtx(msgs, 4096, {
|
|
tools,
|
|
reserveCompletion: 512
|
|
})
|
|
t.is(out.length, 2)
|
|
t.ok(String(/** @type {any} */ (out[0]).content).length < 20_000)
|
|
t.ok(
|
|
bareAgentEstimateTokens(out) + bareAgentEstimateTokens(tools) + 512 <=
|
|
4096 + 200
|
|
)
|
|
})
|
|
|
|
test('advanced compaction summarizes older turns instead of dropping blindly', async (t) => {
|
|
const { bareAgentCompactMessagesForCtx, bareAgentIsCompactionMessage } = load()
|
|
/** @type {unknown[]} */
|
|
const msgs = [{ role: 'system', content: 'You are Bare OS agent.' }]
|
|
for (let i = 0; i < 20; i++) {
|
|
msgs.push({
|
|
role: 'user',
|
|
content: 'Please inspect /tmp/file-' + i + ' and report status code ' + i
|
|
})
|
|
msgs.push({
|
|
role: 'assistant',
|
|
content: null,
|
|
tool_calls: [
|
|
{
|
|
id: 'c' + i,
|
|
type: 'function',
|
|
function: {
|
|
name: 'run_command',
|
|
arguments: JSON.stringify({ command: 'cat /tmp/file-' + i })
|
|
}
|
|
}
|
|
]
|
|
})
|
|
msgs.push({
|
|
role: 'tool',
|
|
tool_call_id: 'c' + i,
|
|
name: 'run_command',
|
|
content: 'EXIT:0\n' + ('payload-' + i + '-').repeat(400)
|
|
})
|
|
msgs.push({
|
|
role: 'assistant',
|
|
content: 'File ' + i + ' looks fine with exit 0.'
|
|
})
|
|
}
|
|
msgs.push({ role: 'user', content: 'What did we learn about the last files?' })
|
|
|
|
const packed = bareAgentCompactMessagesForCtx(msgs, 4096, {
|
|
reserveCompletion: 256,
|
|
mode: 'aggressive',
|
|
keepRecent: 3,
|
|
toolMaxChars: 400,
|
|
summaryMaxChars: 1800
|
|
})
|
|
t.ok(packed.meta.afterTokens <= packed.meta.budget + 80)
|
|
t.ok(packed.meta.compacted)
|
|
t.ok(
|
|
packed.meta.droppedGroups > 0 ||
|
|
packed.meta.tiers.includes('full-replace') ||
|
|
packed.meta.tiers.includes('rolling-summary') ||
|
|
packed.meta.tiers.some((x) => String(x).startsWith('tighten-keep'))
|
|
)
|
|
t.ok(packed.messages.some((m) => bareAgentIsCompactionMessage(m)))
|
|
// Last user question survives (Grok assemble keeps it as a wrapped query).
|
|
const blob = packed.messages
|
|
.map((m) => String(/** @type {any} */ (m).content || ''))
|
|
.join('\n')
|
|
t.ok(/last files/.test(blob))
|
|
t.ok(/<user_query>/.test(blob))
|
|
const summary = packed.messages.find((m) => bareAgentIsCompactionMessage(m))
|
|
t.ok(summary)
|
|
t.ok(
|
|
/Primary Request|run_command|\/tmp\/file|Tool Usage/i.test(
|
|
String(/** @type {any} */ (summary).content || '')
|
|
)
|
|
)
|
|
t.ok(
|
|
/This session is being continued from a previous conversation/.test(
|
|
String(/** @type {any} */ (summary).content || '')
|
|
)
|
|
)
|
|
})
|
|
|
|
test('compaction mode off falls back to hard trim only', async (t) => {
|
|
const { bareAgentCompactMessagesForCtx } = load()
|
|
const msgs = [
|
|
{ role: 'system', content: 'S'.repeat(5000) },
|
|
{ role: 'user', content: 'a' },
|
|
{ role: 'assistant', content: 'b'.repeat(5000) },
|
|
{ role: 'user', content: 'c' }
|
|
]
|
|
const packed = bareAgentCompactMessagesForCtx(msgs, 2048, {
|
|
mode: 'off',
|
|
reserveCompletion: 256
|
|
})
|
|
t.is(packed.meta.mode, 'off')
|
|
t.ok(packed.meta.afterTokens <= packed.meta.budget + 100)
|
|
})
|
|
|
|
test('fat tool payloads shrink even under soft budget', async (t) => {
|
|
const { bareAgentCompactMessagesForCtx, bareAgentEstimateTokens } = load()
|
|
const msgs = [
|
|
{ role: 'system', content: 'sys' },
|
|
{ role: 'user', content: 'run it' },
|
|
{
|
|
role: 'tool',
|
|
name: 'run_command',
|
|
content: 'X'.repeat(20_000)
|
|
}
|
|
]
|
|
const before = bareAgentEstimateTokens(msgs)
|
|
const packed = bareAgentCompactMessagesForCtx(msgs, 32768, {
|
|
mode: 'auto',
|
|
reserveCompletion: 512,
|
|
toolMaxChars: 1000
|
|
})
|
|
const tool = /** @type {any} */ (
|
|
packed.messages.find(
|
|
(m) => m && /** @type {any} */ (m).role === 'tool'
|
|
)
|
|
)
|
|
t.ok(tool)
|
|
t.ok(String(tool.content).length < 20_000)
|
|
t.ok(packed.meta.afterTokens < before)
|
|
t.ok(packed.meta.tiers.includes('shrink-tools') || packed.meta.compacted)
|
|
})
|
|
|
|
test('full-replace assemble keeps last user query and structured summary', async (t) => {
|
|
const {
|
|
bareAgentAssembleCompactedHistory,
|
|
bareAgentBuildStructuredSummary,
|
|
bareAgentIsDegenerateSummary,
|
|
bareAgentWrapUserQuery,
|
|
bareAgentFormatCompactSummaryContent,
|
|
bareAgentBeginAutonomousRun,
|
|
bareAgentAutonomousShouldContinue,
|
|
bareAgentAutonomousContinuationPrompt
|
|
} = load()
|
|
const assembled = bareAgentAssembleCompactedHistory({
|
|
system: { role: 'system', content: 'sys' },
|
|
lastUserQuery: 'fix the agent runner',
|
|
recent: [{ role: 'assistant', content: 'looking' }],
|
|
summaryText: bareAgentFormatCompactSummaryContent(
|
|
'1. Primary Request and Intent:\n fix the agent runner\n'
|
|
),
|
|
reminder: '<system-reminder>\nAutonomous run: fix the agent runner\n</system-reminder>'
|
|
})
|
|
t.is(/** @type {any} */ (assembled[0]).role, 'system')
|
|
t.ok(
|
|
String(/** @type {any} */ (assembled[1]).content).includes('<user_query>')
|
|
)
|
|
t.ok(
|
|
String(/** @type {any} */ (assembled[1]).content).includes('fix the agent runner')
|
|
)
|
|
t.is(/** @type {any} */ (assembled[2]).role, 'assistant')
|
|
t.ok(String(/** @type {any} */ (assembled[3]).content).includes('continued from a previous'))
|
|
t.ok(String(/** @type {any} */ (assembled[4]).content).includes('<system-reminder>'))
|
|
|
|
const structured = bareAgentBuildStructuredSummary(
|
|
[
|
|
[
|
|
{ role: 'user', content: 'inspect ~/notes.txt' },
|
|
{
|
|
role: 'tool',
|
|
name: 'read_file',
|
|
content: 'ENOENT missing file'
|
|
}
|
|
]
|
|
],
|
|
{ maxChars: 2000, perMsg: 80 }
|
|
)
|
|
t.ok(/1\. Primary Request/.test(structured))
|
|
t.ok(/3\. Tool Usage/.test(structured))
|
|
t.ok(/notes\.txt|read_file|ENOENT/.test(structured))
|
|
t.absent(bareAgentIsDegenerateSummary(structured + '\n'.repeat(5) + 'x'.repeat(80)))
|
|
t.ok(bareAgentIsDegenerateSummary('too short'))
|
|
t.ok(bareAgentWrapUserQuery('hi').indexOf('<user_query>') === 0)
|
|
|
|
const started = bareAgentBeginAutonomousRun(
|
|
{ autonomous_mode_enabled: false, autonomous_active: false },
|
|
{ goal: 'ship compaction', maxRuntimeMs: 120000 }
|
|
)
|
|
t.ok(started.autonomous_mode_enabled)
|
|
t.ok(started.autonomous_active)
|
|
t.is(started.autonomous_goal, 'ship compaction')
|
|
t.ok(
|
|
bareAgentAutonomousShouldContinue(started, {
|
|
completed: false,
|
|
hasTools: false,
|
|
stopRequested: false
|
|
})
|
|
)
|
|
t.absent(
|
|
bareAgentAutonomousShouldContinue(started, {
|
|
completed: true,
|
|
hasTools: false
|
|
})
|
|
)
|
|
t.ok(
|
|
/AUTONOMOUS RUN still active/.test(
|
|
bareAgentAutonomousContinuationPrompt(started, { remainingMs: 90000 })
|
|
)
|
|
)
|
|
})
|