Merge master into codex/simp-session-log-representation

This commit is contained in:
Tianyi Cui
2026-07-17 22:33:42 +08:00
310 changed files with 4718 additions and 1903 deletions

View File

@@ -11,12 +11,12 @@ This backend owns the compaction policy:
- **Estimation** — a configurable characters-per-token heuristic counts the current session prefix supplied to pre-step, derived history, and system prompt, matching the next request rather than stale logged prefix state.
- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes; a single unit larger than the budget remains out of scope.
- **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold.
- **Summarization** — a direct `llm/stream` call uses the configured model and cap without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call.
- **Summarization** — a direct `llm/stream` call uses the configured provider/model pair and cap, falling back to the latest logged request target and then the agent target, without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call.
- **Framing** — the replacement user message marks established checkpoint context with `<compacted-summary>` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint.
- **Lifecycle** — `compactRegion()` records its start, summary, replacement, and end. The serial `agent/pre-step` listener checks pressure before every step, outside an open step, so a tool-heavy turn remains compactable and the loop derives history once after mutation.
- **Failure handling** — an unmatched `compact/start` is an inert crash marker because no replacement landed. Recoverable failure records an error end and leaves the surface unchanged.
`estimateContentTokens()` and `summarize()` are overridable hooks: a tokenizer-based or template-based backend can subclass `BasicCompactService` and override just those, reusing the retention walk and surface plumbing. `summarize()` returns the summary blocks together with the call envelope it actually used (`{ summary, model, maxTokens? }`) — the caller logs that envelope on the `compact/summary` provenance event, so an overriding backend reports its own envelope honestly.
`estimateContentTokens()` and `summarize()` are overridable hooks: a tokenizer-based or template-based backend can subclass `BasicCompactService` and override just those, reusing the retention walk and surface plumbing. `summarize()` returns the summary blocks together with the call envelope it actually used (`{ summary, provider, model, maxTokens? }`) — the caller logs that envelope on the `compact/summary` provenance event, so an overriding backend reports its own envelope honestly.
## Config (`BasicCompactConfig`)
@@ -27,7 +27,8 @@ Every knob is **required** except `auto` — there is no concrete data yet to ju
| `contextWindow` | yes | Context window size in tokens. |
| `thresholdRatio` | yes | Compact when estimated usage exceeds this fraction of the window. |
| `retainTokens` | yes | Tokens of recent context to keep intact. |
| `summarizationModel` | yes | Model for summarization (`''` → use the agent's model). |
| `summarizationProvider` | yes | Provider for summarization (`''` together with an empty model → use the latest logged request pair, then the agent pair). |
| `summarizationModel` | yes | Model for summarization (`''` together with an empty provider → use the latest logged request pair, then the agent pair). |
| `maxTokens` | yes | Provider generation cap for the summarization call; may include reasoning tokens. |
| `compactionRetries` | yes | Extra compaction attempts after the first if the compacted surface remains over threshold. |
| `auto` | no (default `true`) | Register the `agent/pre-step` auto-compaction listener. Set `false` for manual-only. |
@@ -47,6 +48,7 @@ export function apply(ctx: Context): void {
contextWindow: 128000,
thresholdRatio: 0.8,
retainTokens: 20480,
summarizationProvider: '',
summarizationModel: '',
maxTokens: 8192,
compactionRetries: 1,

View File

@@ -227,21 +227,26 @@ export class BasicCompactService extends CompactService {
/**
* Summarize through a direct one-shot `ctx.llm.stream()` call, not an agent
* step or `agent/request` dispatch. Failure finishes and truncated summaries
* reject; the signal is forwarded and only text reaches the checkpoint.
* reject; the signal is forwarded, only text reaches the checkpoint, and the
* returned envelope identifies the provider/model actually used.
*
* @param text - plain-text rendering of the conversation region to condense.
* @param agent - supplies the fallback model and the session id stamped on
* the call; throws when neither it nor the config names a model.
* @param agent - supplies the request-header/creation fallback target and the
* session id stamped on the call; throws when no complete target exists.
* @param signal - optional abort signal, forwarded into the model call.
* @returns the text-only summary blocks plus the call envelope used
* (`model`, and `maxTokens` when the summarizer has a cap).
* (`provider`, `model`, and `maxTokens` when the summarizer has a cap).
*/
async summarize(
text: string, agent: Agent, signal?: AbortSignal,
): Promise<{ summary: ContentBlock[]; model: string; maxTokens?: number }> {
): Promise<{ summary: ContentBlock[]; provider: string; model: string; maxTokens?: number }> {
const assembler = new BlockAssembler()
const logged = agent.session.requestHeader()?.config
const provider = this.config.summarizationProvider || logged?.provider || agent.options.provider || ''
const model = this.config.summarizationModel || logged?.model || agent.options.model || ''
const options: GenerateOptions = {
model: this.config.summarizationModel || agent.options.model || '',
provider,
model,
messages: [{
role: 'user',
content: [{ type: 'text', text: `Summarize this conversation history:\n\n${text}\n\nSummary:` }],
@@ -253,8 +258,8 @@ export class BasicCompactService extends CompactService {
// exactOptionalPropertyTypes: only set `signal` when present — assigning
// `undefined` to an optional `signal?: AbortSignal` is a type error.
if (signal) options.signal = signal
if (!options.model) {
throw new Error('no model available for summarization: set BasicCompactConfig.summarizationModel or AgentOptions.model')
if (!options.provider || !options.model) {
throw new Error('no provider/model available for summarization: set both summarization fields or provide a logged/agent target')
}
for await (const chunk of this.ctx.llm.stream(options)) {
assembler.push(chunk)
@@ -271,7 +276,7 @@ export class BasicCompactService extends CompactService {
// config.maxTokens is required and validated positive, so this backend's
// envelope always carries the cap; the return type's optionality exists
// for overriding subclasses whose summarizer has none.
return { summary, model: options.model, maxTokens: this.config.maxTokens }
return { summary, provider: options.provider, model: options.model, maxTokens: this.config.maxTokens }
}
// ---- Core API (implements the abstract contract) ----
@@ -378,7 +383,7 @@ export class BasicCompactService extends CompactService {
try {
// --- Extract text and summarize ---
const text = renderTranscript(session.events, shadowedSeqs)
const { summary, model, maxTokens } = await this.summarize(text, agent, signal)
const { summary, provider, model, maxTokens } = await this.summarize(text, agent, signal)
// Estimate token count of the shadowed content for provenance.
let shadowedTokenCount = 0
@@ -400,6 +405,7 @@ export class BasicCompactService extends CompactService {
shadowedRange: { start, end },
shadowedSeqs,
shadowedTokenCount,
provider,
model,
...maxTokens !== undefined ? { maxTokens } : {},
})

View File

@@ -24,7 +24,9 @@ export interface BasicCompactConfig {
thresholdRatio: number
/** Number of tokens of recent context to retain during compaction. */
retainTokens: number
/** Model to use for summarization (`''` — uses the agent's model). */
/** Provider to use for summarization (`''` with an empty model inherits the conversation target). */
summarizationProvider: string
/** Model to use for summarization (`''` with an empty provider inherits the conversation target). */
summarizationModel: string
/** Provider generation cap for the summarization call. */
maxTokens: number
@@ -63,6 +65,12 @@ export function resolveConfig(config: BasicCompactConfig): ResolvedConfig {
if (typeof resolved.summarizationModel !== 'string') {
throw new Error('BasicCompactConfig: summarizationModel must be a string.')
}
if (typeof resolved.summarizationProvider !== 'string') {
throw new Error('BasicCompactConfig: summarizationProvider must be a string.')
}
if ((resolved.summarizationProvider.length === 0) !== (resolved.summarizationModel.length === 0)) {
throw new Error('BasicCompactConfig: summarizationProvider and summarizationModel must both be empty or both be set.')
}
if (typeof resolved.auto !== 'boolean') {
throw new Error('BasicCompactConfig: auto must be a boolean.')
}

View File

@@ -21,6 +21,7 @@ const TEST_CONFIG: BasicCompactConfig = {
contextWindow: 128000,
thresholdRatio: 0.8,
retainTokens: 20480,
summarizationProvider: '',
summarizationModel: '',
maxTokens: 8192,
compactionRetries: 1,
@@ -58,13 +59,17 @@ class TestCompactService extends BasicCompactService {
return blocks.length * 10
}
override async summarize(text: string, agent: Agent): Promise<{ summary: ContentBlock[]; model: string; maxTokens?: number }> {
override async summarize(
text: string,
agent: Agent,
): Promise<{ summary: ContentBlock[]; provider: string; model: string; maxTokens?: number }> {
const provider = this.config.summarizationProvider || agent.options.provider || ''
const model = this.config.summarizationModel || agent.options.model || ''
this.summarizeCalls.push({ text, model })
if (this.summarizeError) throw this.summarizeError
const summary = this.mockSummaryQueue.shift() ?? this.mockSummary
this.summaryOutputs.add(summary)
return { summary, model }
return { summary, provider, model }
}
}
@@ -94,7 +99,7 @@ function multiTurnSession(turns: number, messagesPerTurn: number = 2, opts: { le
content: [{ type: 'text', text: `turn ${t} user message ${m + 1}.${LONG_FIXTURE_TEXT}` }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: t, step: 1,
content: [{ type: 'text', text: `turn ${t} assistant response ${m + 1}.${LONG_FIXTURE_TEXT}` }],
}, { surfaceOp: 'append' })
@@ -119,7 +124,7 @@ function sessionWithTools(): Session {
content: [{ type: 'text', text: 'read file x' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [
{ type: 'text', text: 'Let me read that file.' },
@@ -132,7 +137,7 @@ function sessionWithTools(): Session {
content: [{ type: 'text', text: 'hello world' }],
isError: false,
}, { surfaceOp: 'append' })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [{ type: 'text', text: 'The file contains: hello world' }],
}, { surfaceOp: 'append' })
@@ -161,7 +166,7 @@ function toolTurnSession(turns: number): Session {
source: { kind: 'user' },
}, { surfaceOp: 'append' })
s.append('step/start', { turn: t, step: 1 })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: t, step: 1,
content: [
{ type: 'text', text: `turn ${t} calling tool` },
@@ -224,7 +229,7 @@ describe('BasicCompactService step-alignment (never split a tool-call/result pai
const s = new Session(SessionId('one-step'))
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('step/start', { turn: 1, step: 1 })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [{ type: 'text', text: 'calling' }, { type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
@@ -270,7 +275,7 @@ describe('BasicCompactService step-alignment (never split a tool-call/result pai
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('user/message', { content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('step/start', { turn: 1, step: 1 })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [{ type: 'tool-call', id: CallId('c1'), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
@@ -332,7 +337,7 @@ describe('BasicCompactService.estimateEventTokens', () => {
const userEvent: SessionEvent = { type: 'user/message', seq: 0, time: 1, data: { content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } } }
expect(svc.estimateEventTokens(userEvent)).toBe(10)
const asstEvent: SessionEvent = { type: 'assistant/message', seq: 1, time: 2, data: { turn: 1, step: 1, content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }] } }
const asstEvent: SessionEvent = { type: 'assistant/message', seq: 1, time: 2, data: { turn: 1, step: 1, content: [{ type: 'text', text: 'a' }, { type: 'text', text: 'b' }], provenance: { provider: 'mock', model: 'mock' } } }
expect(svc.estimateEventTokens(asstEvent)).toBe(20)
const toolEvent: SessionEvent = { type: 'tool/result', seq: 2, time: 3, data: { turn: 1, step: 1, callId: CallId('c1'), content: [{ type: 'text', text: 'output' }], isError: false } }
@@ -606,7 +611,7 @@ describe('BasicCompactService.compactIfNeeded', () => {
s.append('user/message', { content: [{ type: 'text', text: 'do a big multi-step task' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
for (let step = 1; step <= 5; step++) {
s.append('step/start', { turn: 1, step })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step,
content: [{ type: 'text', text: `step ${step}` }, { type: 'tool-call', id: CallId(`c${step}`), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
@@ -654,7 +659,7 @@ describe('BasicCompactService.compactIfNeeded', () => {
// the fresh nodes are retained.
s.append('step/start', { turn: 5, step: 1 })
s.append('user/message', { content: [{ type: 'text', text: 'turn 5 work' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('assistant/message', { turn: 5, step: 1, content: [{ type: 'text', text: 'reply 5' }] }, { surfaceOp: 'append' })
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 5, step: 1, content: [{ type: 'text', text: 'reply 5' }] }, { surfaceOp: 'append' })
s.append('step/end', { turn: 5, step: 1 })
const second = await compactIfNeeded(svc, s, '', 'm', SIGNAL)
@@ -751,7 +756,7 @@ describe('BasicCompactService blocking (compaction in progress)', () => {
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('step/start', { turn: 1, step: 1 })
s.append('user/message', { content: [{ type: 'text', text: 'turn 1' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'text', text: 'reply 1' }] }, { surfaceOp: 'append' })
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'text', text: 'reply 1' }] }, { surfaceOp: 'append' })
s.append('compact/start', { turn: 1 }) // ← orphaned: no matching compact/end
s.append('step/end', { turn: 1, step: 1 })
s.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) // repair closed the turn
@@ -954,7 +959,7 @@ async function ctxWithFinish(reason: (StreamChunk & { type: 'finish' })['reason'
/** A minimal Agent stub carrying just session + options (enough for the listeners). */
function stubAgent(session: Session, model?: string): Agent {
return { session, options: { model } } as unknown as Agent
return { session, options: { provider: model, model } } as unknown as Agent
}
function compactIfNeeded(
@@ -1039,7 +1044,7 @@ describe('BasicCompactService.summarize (real ctx.llm.stream)', () => {
it('throws when no model is provided', async () => {
const { ctx } = await ctxWithModel('x')
const svc = new BasicCompactService(ctx, cfg({ auto: false }))
await expect(summarize(svc, 'text', '')).rejects.toThrow(/no model available/)
await expect(summarize(svc, 'text', '')).rejects.toThrow(/no provider\/model available/)
})
it('rethrows when the stream ends with a finish-error chunk', async () => {
@@ -1116,7 +1121,7 @@ describe('BasicCompactService.summarize (real ctx.llm.stream)', () => {
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('step/start', { turn: 1, step: 1 })
session.append('user/message', { content: [{ type: 'text', text: 'tiny user' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'text', text: 'tiny assistant' }] }, { surfaceOp: 'append' })
session.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'text', text: 'tiny assistant' }] }, { surfaceOp: 'append' })
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
@@ -1225,6 +1230,7 @@ describe('BasicCompactService auto-compaction (agent/pre-step listener)', () =>
// One-shot summaries bypass agent/request but remain mutable at llm/stream;
// adapter selection happens after the waterfall rewrite.
ctx.on('llm/stream', (options, next) => {
options.provider = 'routed-model'
options.model = 'routed-model'
return next()
})
@@ -1267,7 +1273,7 @@ describe('BasicCompactService transcript rendering (delegated to dsh-compact)',
content: [{ type: 'text', text: 'project context here' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [{ type: 'reasoning', text: 'thinking hard' }, { type: 'text', text: 'answer' }],
}, { surfaceOp: 'append' })
@@ -1295,7 +1301,7 @@ describe('BasicCompactService transcript rendering (delegated to dsh-compact)',
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('step/start', { turn: 1, step: 1 })
s.append('user/message', { content: [{ type: 'text', text: 'run it' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [{ type: 'tool-call', id: CallId('c9'), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
@@ -1324,7 +1330,7 @@ describe('BasicCompactService edge cases', () => {
// assistant/message carrying a nested tool-result block, an unknown block,
// and the tool-call that the following tool/result answers (so the surface
// is tool-pairing balanced).
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [
{ type: 'tool-result', toolCallId: CallId('n1'), content: [{ type: 'chart', data: 'x' } as unknown as ContentBlock] },
@@ -1387,7 +1393,7 @@ describe('BasicCompactService edge cases', () => {
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('step/start', { turn: 1, step: 1 })
s.append('user/message', { content: [{ type: 'text', text: 'orphan' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'text', text: 'reply' }] }, { surfaceOp: 'append' })
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'text', text: 'reply' }] }, { surfaceOp: 'append' })
s.append('step/end', { turn: 1, step: 1 })
s.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
const nodes = s.surface.nodes
@@ -1479,13 +1485,13 @@ describe('BasicCompactService edge cases', () => {
s.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
s.append('step/start', { turn: 1, step: 1 })
s.append('user/message', { content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('assistant/message', { turn: 1, step: 1, content: [{ type: 'reasoning', text: '' }] }, { surfaceOp: 'append' })
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn: 1, step: 1, content: [{ type: 'reasoning', text: '' }] }, { surfaceOp: 'append' })
s.append('context/message', { content: [], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('steering/message', { turn: 1, content: [{ type: 'text', text: '' }], source: { kind: 'user' } }, { surfaceOp: 'append' })
s.append('step/end', { turn: 1, step: 1 })
// Keep the log pairing-valid while the empty result covers the final message kind.
s.append('step/start', { turn: 1, step: 2 })
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 2,
content: [{ type: 'tool-call', id: CallId('z1'), name: 'bash', arguments: '{}' }],
}, { surfaceOp: 'append' })
@@ -1516,7 +1522,7 @@ describe('BasicCompactService edge cases', () => {
s.append('user/message', { content: [chart('y')], source: { kind: 'user' } }, { surfaceOp: 'append' })
// assistant/message with a plugin-added block AND the tool-call its
// tool/result answers (so the surface is tool-pairing balanced).
s.append('assistant/message', {
s.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' },
turn: 1, step: 1,
content: [
chart('z'),
@@ -1641,7 +1647,7 @@ describe('BasicCompactService under the real invariants plugin', () => {
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('step/start', { turn, step: 1 })
session.append('user/message', { content: [{ type: 'text', text: `turn ${turn} user.${LONG_FIXTURE_TEXT}` }], source: { kind: 'user' } }, { surfaceOp: 'append' })
session.append('assistant/message', { turn, step: 1, content: [{ type: 'text', text: `turn ${turn} assistant.${LONG_FIXTURE_TEXT}` }] }, { surfaceOp: 'append' })
session.append('assistant/message', { provenance: { provider: 'mock', model: 'mock' }, turn, step: 1, content: [{ type: 'text', text: `turn ${turn} assistant.${LONG_FIXTURE_TEXT}` }] }, { surfaceOp: 'append' })
session.append('step/end', { turn, step: 1 })
session.append('turn/end', { turn, reason: { kind: 'completed' } })
}

View File

@@ -25,8 +25,8 @@ class ReproCompactService extends BasicCompactService {
return blocks.length * TOKENS_PER_BLOCK
}
override async summarize(): Promise<{ summary: ContentBlock[]; model: string }> {
return { summary: [{ type: 'text', text: 'CHECKPOINT SUMMARY' }], model: 'stub' }
override async summarize(): Promise<{ summary: ContentBlock[]; provider: string; model: string }> {
return { summary: [{ type: 'text', text: 'CHECKPOINT SUMMARY' }], provider: 'mock', model: 'stub' }
}
}
@@ -77,6 +77,7 @@ async function harness(toolSteps: number): Promise<{ ctx: Context; compact: Repr
contextWindow: 64,
thresholdRatio: 0.5,
retainTokens: 20,
summarizationProvider: '',
summarizationModel: '',
maxTokens: 8192,
compactionRetries: 1,
@@ -99,7 +100,7 @@ describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', ()
it('the head checkpoint the loop lands is a balanced cut on both sides', async () => {
const { ctx } = await harness(8)
try {
const agent = ctx.agentLoop.create(AgentId('repro'), { model: 'mock' })
const agent = ctx.agentLoop.create(AgentId('repro'), { provider: 'mock', model: 'mock' })
agent.send([{ type: 'text', text: 'do a long multi-step task' }])
await waitForIdle(ctx, agent)