mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge latest origin/master into token-meter-service
This commit is contained in:
@@ -11,12 +11,12 @@ This backend owns the compaction policy:
|
||||
- **Measurement** — the singleton `ctx.tokenMeter` prices the provisional request envelope and current surface at one consumed-log revision. The current prompt and prefix override their logged values; the pre-step boundary reuses logged tools and call config.
|
||||
- **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()` requires its agent to own the exact target session and rejects mismatch before resolution or mutation; a valid call 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.
|
||||
|
||||
`summarize()` is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the summary blocks together with the call envelope it used (`{ summary, model, maxTokens? }`), which is logged on `compact/summary`.
|
||||
`summarize()` is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the summary blocks together with the call envelope it used (`{ summary, provider, model, maxTokens? }`), which is logged on `compact/summary`.
|
||||
|
||||
## Config (`BasicCompactConfig`)
|
||||
|
||||
@@ -26,7 +26,8 @@ Every setting is optional. The pressure and retention policy applies to the toke
|
||||
|---|---|---|
|
||||
| `thresholdRatio` | no (default `0.8`) | Compact at `floor(contextWindow × ratio)`. |
|
||||
| `retainTokens` | no (default `floor(contextWindow × 0.16)`) | Recent surface budget kept verbatim; must be below the threshold. |
|
||||
| `summarizationModel` | no (default `''`) | Empty resolves the latest logged routed model, then `AgentOptions.model`. |
|
||||
| `summarizationProvider` | no (default `''`) | Set together with `summarizationModel`; an empty pair resolves the latest logged request target, then the `AgentOptions` pair. |
|
||||
| `summarizationModel` | no (default `''`) | Set together with `summarizationProvider`; an empty pair resolves the latest logged request target, then the `AgentOptions` pair. |
|
||||
| `maxTokens` | no (default `8192`) | Provider generation cap for the summarization call; may include reasoning tokens. |
|
||||
| `compactionRetries` | no (default `1`) | Extra attempts after the first when pressure remains above threshold. |
|
||||
| `auto` | no (default `true`) | Register the `agent/pre-step` automatic listener. Set `false` for manual-only. |
|
||||
@@ -115,7 +116,7 @@ Rules:
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **Pre-step sees a provisional request envelope** — the current prompt and prefix are exact, but routing and tool changes made later in `agent/request` are not logged yet. A router-only agent with no provisional model skips that check.
|
||||
- **Pre-step sees a provisional request envelope** — the current prompt and prefix are exact, but routing and tool changes made later in `agent/request` are not logged yet. A router-only agent with no provisional provider/model pair skips that check.
|
||||
- **Meter accuracy follows the fixed heuristic** — missing reusable provider usage falls back to character count plus structural overhead rather than exact tokenization.
|
||||
- **`compactRegion` requires an open turn** — a manual call on a fully-closed session throws ("no open turn") rather than compacting.
|
||||
- **Summarization failure fails closed with full, over-budget history** — including truncation at the summarization `maxTokens`, which hidden reasoning tokens can consume; the auto path logs a warning and proceeds.
|
||||
|
||||
@@ -18,6 +18,7 @@ const DEFAULT_RETAIN_RATIO = 0.16
|
||||
const BASIC_COMPACT_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
||||
'thresholdRatio',
|
||||
'retainTokens',
|
||||
'summarizationProvider',
|
||||
'summarizationModel',
|
||||
'maxTokens',
|
||||
'compactionRetries',
|
||||
@@ -30,7 +31,7 @@ function validateConfigKeys(config: BasicCompactConfig): void {
|
||||
if (!BASIC_COMPACT_CONFIG_KEYS.has(key)) {
|
||||
throw new Error(
|
||||
`BasicCompactConfig: unknown key "${key}" `
|
||||
+ '(allowed: thresholdRatio, retainTokens, summarizationModel, maxTokens, compactionRetries, auto)',
|
||||
+ '(allowed: thresholdRatio, retainTokens, summarizationProvider, summarizationModel, maxTokens, compactionRetries, auto)',
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -53,6 +54,7 @@ export function resolveConfig(
|
||||
const resolved: ResolvedConfig = {
|
||||
thresholdRatio,
|
||||
retainTokens,
|
||||
summarizationProvider: config.summarizationProvider ?? '',
|
||||
summarizationModel: config.summarizationModel ?? '',
|
||||
maxTokens: config.maxTokens ?? 8192,
|
||||
compactionRetries: config.compactionRetries ?? 1,
|
||||
@@ -69,9 +71,17 @@ export function resolveConfig(
|
||||
}
|
||||
assertPositiveInteger('maxTokens', resolved.maxTokens)
|
||||
assertNonNegativeInteger('compactionRetries', resolved.compactionRetries)
|
||||
if (typeof resolved.summarizationProvider !== 'string') {
|
||||
throw new Error('BasicCompactConfig: summarizationProvider must be a string')
|
||||
}
|
||||
if (typeof resolved.summarizationModel !== 'string') {
|
||||
throw new Error('BasicCompactConfig: summarizationModel must be a string')
|
||||
}
|
||||
if ((resolved.summarizationProvider.length === 0) !== (resolved.summarizationModel.length === 0)) {
|
||||
throw new Error(
|
||||
'BasicCompactConfig: summarizationProvider and summarizationModel must both be set or both be empty',
|
||||
)
|
||||
}
|
||||
if (typeof resolved.auto !== 'boolean') {
|
||||
throw new Error('BasicCompactConfig: auto must be a boolean')
|
||||
}
|
||||
|
||||
@@ -27,9 +27,15 @@ export type {
|
||||
ResolvedConfig,
|
||||
} from './types.ts'
|
||||
|
||||
/** Resolve the latest actual routed model, then the agent's configured fallback. */
|
||||
function effectiveModel(agent: Agent): string | undefined {
|
||||
return agent.session.requestHeader()?.config.model ?? agent.options.model
|
||||
/** Resolve the latest actual routed provider/model, then the complete agent fallback pair. */
|
||||
function effectiveTarget(agent: Agent): { provider: string; model: string } | undefined {
|
||||
const latest = agent.session.requestHeader()?.config
|
||||
if (latest !== undefined) return { provider: latest.provider, model: latest.model }
|
||||
const { provider, model } = agent.options
|
||||
if (provider === undefined || provider.length === 0 || model === undefined || model.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return { provider, model }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,14 +44,14 @@ function effectiveModel(agent: Agent): string | undefined {
|
||||
* later request middleware has not run yet.
|
||||
*/
|
||||
function provisionalHeader(
|
||||
model: string,
|
||||
target: { provider: string; model: string },
|
||||
session: Session,
|
||||
fullSystemPrompt: string,
|
||||
sessionPrefix: readonly Message[],
|
||||
): EpochHeader {
|
||||
const latest = session.requestHeader()
|
||||
return canonicalHeader({
|
||||
config: latest === undefined ? { model } : { ...latest.config, model },
|
||||
config: latest === undefined ? target : { ...latest.config, ...target },
|
||||
...fullSystemPrompt.length === 0 ? {} : { system: fullSystemPrompt },
|
||||
...latest?.tools === undefined ? {} : { tools: latest.tools },
|
||||
...sessionPrefix.length === 0 ? {} : { messagePrefix: [...sessionPrefix] },
|
||||
@@ -66,6 +72,7 @@ export class BasicCompactService extends CompactService {
|
||||
static Config: z<BasicCompactConfig> = z.object({
|
||||
thresholdRatio: z.number().default(0.8),
|
||||
retainTokens: z.number().step(1),
|
||||
summarizationProvider: z.string().default(''),
|
||||
summarizationModel: z.string().default(''),
|
||||
maxTokens: z.number().step(1).min(1).default(8192),
|
||||
compactionRetries: z.number().step(1).min(0).default(1),
|
||||
@@ -93,7 +100,7 @@ export class BasicCompactService extends CompactService {
|
||||
text: string,
|
||||
agent: Agent,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ summary: ContentBlock[]; model: string; maxTokens?: number }> {
|
||||
): Promise<{ summary: ContentBlock[]; provider: string; model: string; maxTokens?: number }> {
|
||||
return summarizeWithLlm(this.ctx, this.config, text, agent, signal)
|
||||
}
|
||||
|
||||
@@ -101,7 +108,7 @@ export class BasicCompactService extends CompactService {
|
||||
* Check replayed pressure for the provisional pre-step envelope and compact
|
||||
* a tool-balanced head until it falls below the service-wide threshold.
|
||||
* A genuinely model-less router-first step skips this provisional check.
|
||||
* @param agent - agent whose session and provisional model are measured.
|
||||
* @param agent - agent whose session and provisional provider/model are measured.
|
||||
* @param fullSystemPrompt - current assembled system prompt override.
|
||||
* @param sessionPrefix - current request-only prefix override.
|
||||
* @param signal - live step cancellation signal forwarded to summarization.
|
||||
@@ -113,10 +120,10 @@ export class BasicCompactService extends CompactService {
|
||||
sessionPrefix: readonly Message[],
|
||||
signal: AbortSignal,
|
||||
): Promise<CompactionResult | null> {
|
||||
const model = effectiveModel(agent)
|
||||
if (model === undefined || model.length === 0) return null
|
||||
const target = effectiveTarget(agent)
|
||||
if (target === undefined) return null
|
||||
const meter = this.ctx.tokenMeter
|
||||
const requestHeader = provisionalHeader(model, agent.session, fullSystemPrompt, sessionPrefix)
|
||||
const requestHeader = provisionalHeader(target, agent.session, fullSystemPrompt, sessionPrefix)
|
||||
const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio)
|
||||
let measurement = meter.measure(agent.session, requestHeader)
|
||||
if (measurement.totalTokens < threshold) return null
|
||||
|
||||
@@ -123,7 +123,7 @@ export async function compactSurfaceRegion(
|
||||
}
|
||||
const shadowedTokenCount = selected.reduce((total, node) => total + node.tokens, 0)
|
||||
const text = renderTranscript(session.events, shadowedSeqs)
|
||||
const { summary, model, maxTokens } = await dependencies.summarize(text, agent, signal)
|
||||
const { summary, provider, model, maxTokens } = await dependencies.summarize(text, agent, signal)
|
||||
|
||||
const currentMeasurement = dependencies.meter.measure(session)
|
||||
if (currentMeasurement.logRevision !== lockedMeasurement.logRevision) {
|
||||
@@ -145,6 +145,7 @@ export async function compactSurfaceRegion(
|
||||
shadowedRange: { start, end },
|
||||
shadowedSeqs,
|
||||
shadowedTokenCount,
|
||||
provider,
|
||||
model,
|
||||
...maxTokens === undefined ? {} : { maxTokens },
|
||||
})
|
||||
|
||||
@@ -58,6 +58,7 @@ const CHECKPOINT_PREAMBLE =
|
||||
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
|
||||
export interface SummaryResult {
|
||||
summary: ContentBlock[]
|
||||
provider: string
|
||||
model: string
|
||||
maxTokens?: number
|
||||
}
|
||||
@@ -78,17 +79,27 @@ export async function summarizeWithLlm(
|
||||
agent: Agent,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SummaryResult> {
|
||||
const latestModel = agent.session.requestHeader()?.config.model
|
||||
const model = config.summarizationModel || latestModel || agent.options.model || ''
|
||||
if (model.length === 0) {
|
||||
const latest = agent.session.requestHeader()?.config
|
||||
const configured = config.summarizationProvider.length === 0
|
||||
? undefined
|
||||
: { provider: config.summarizationProvider, model: config.summarizationModel }
|
||||
const agentTarget = agent.options.provider !== undefined
|
||||
&& agent.options.provider.length > 0
|
||||
&& agent.options.model !== undefined
|
||||
&& agent.options.model.length > 0
|
||||
? { provider: agent.options.provider, model: agent.options.model }
|
||||
: undefined
|
||||
const target = configured ?? latest ?? agentTarget
|
||||
if (target === undefined) {
|
||||
throw new Error(
|
||||
'no model available for summarization: set BasicCompactConfig.summarizationModel, route one request, or set AgentOptions.model',
|
||||
'no provider/model available for summarization: set both BasicCompactConfig summarization fields, route one request, or set both AgentOptions fields',
|
||||
)
|
||||
}
|
||||
|
||||
const assembler = new BlockAssembler()
|
||||
const options: GenerateOptions = {
|
||||
model,
|
||||
provider: target.provider,
|
||||
model: target.model,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: `Summarize this conversation history:\n\n${text}\n\nSummary:` }],
|
||||
@@ -106,7 +117,12 @@ export async function summarizeWithLlm(
|
||||
if (!summary.some(block => block.text.trim().length > 0)) {
|
||||
throw new Error('summarization produced no text summary content')
|
||||
}
|
||||
return { summary, model, maxTokens: config.maxTokens }
|
||||
return {
|
||||
summary,
|
||||
provider: target.provider,
|
||||
model: target.model,
|
||||
maxTokens: config.maxTokens,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,9 @@ export interface BasicCompactConfig {
|
||||
thresholdRatio?: number
|
||||
/** Recent surface tokens retained verbatim. Defaults to `floor(contextWindow * 0.16)`. */
|
||||
retainTokens?: number
|
||||
/** Summary model; `''` resolves the latest routed model, then `AgentOptions.model`. Defaults to `''`. */
|
||||
/** Summary provider; `''` resolves the latest routed pair, then the agent pair. Defaults to `''`. */
|
||||
summarizationProvider?: string
|
||||
/** Summary model; `''` resolves the latest routed pair, then the agent pair. Defaults to `''`. */
|
||||
summarizationModel?: string
|
||||
/** Provider generation cap for summarization. Defaults to `8192`. */
|
||||
maxTokens?: number
|
||||
@@ -24,6 +26,7 @@ export interface BasicCompactConfig {
|
||||
export interface ResolvedConfig {
|
||||
readonly thresholdRatio: number
|
||||
readonly retainTokens: number
|
||||
readonly summarizationProvider: string
|
||||
readonly summarizationModel: string
|
||||
readonly maxTokens: number
|
||||
readonly compactionRetries: number
|
||||
|
||||
@@ -20,7 +20,7 @@ function createContext(contextWindow = 1_000): Context {
|
||||
}
|
||||
|
||||
function agent(session: Session, model?: string): Agent {
|
||||
return { session, options: model === undefined ? {} : { model } } as Agent
|
||||
return { session, options: model === undefined ? {} : { provider: model, model } } as Agent
|
||||
}
|
||||
|
||||
/** Closed two-message turns followed by one open turn for durable compaction events. */
|
||||
@@ -34,6 +34,7 @@ function conversation(turns = 4, text = 'fixture '.repeat(40).trim()): Session {
|
||||
}, { surfaceOp: 'append' })
|
||||
session.append('step/start', { turn, step: 1 })
|
||||
session.append('assistant/message', {
|
||||
provenance: { provider: MODEL, model: MODEL },
|
||||
turn,
|
||||
step: 1,
|
||||
content: [{ type: 'text', text: `${text} assistant ${turn}` }],
|
||||
@@ -59,6 +60,7 @@ function toolConversation(): Session {
|
||||
}, { surfaceOp: 'append' })
|
||||
session.append('step/start', { turn, step: 1 })
|
||||
session.append('assistant/message', {
|
||||
provenance: { provider: MODEL, model: MODEL },
|
||||
turn,
|
||||
step: 1,
|
||||
content: [
|
||||
@@ -83,6 +85,7 @@ function toolConversation(): Session {
|
||||
|
||||
class TestCompactService extends BasicCompactService {
|
||||
summary: ContentBlock[] = [{ type: 'text', text: 'small checkpoint' }]
|
||||
summaryProvider = 'summary-provider'
|
||||
summaryModel = 'summary-model'
|
||||
error: unknown
|
||||
mutateDuringSummary: (() => void) | undefined
|
||||
@@ -92,11 +95,16 @@ class TestCompactService extends BasicCompactService {
|
||||
text: string,
|
||||
_agent: Agent,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ summary: ContentBlock[]; model: string; maxTokens?: number }> {
|
||||
): Promise<{ summary: ContentBlock[]; provider: string; model: string; maxTokens?: number }> {
|
||||
this.calls.push({ text, signal })
|
||||
this.mutateDuringSummary?.()
|
||||
if (this.error !== undefined) throw this.error
|
||||
return { summary: this.summary, model: this.summaryModel, maxTokens: 123 }
|
||||
return {
|
||||
summary: this.summary,
|
||||
provider: this.summaryProvider,
|
||||
model: this.summaryModel,
|
||||
maxTokens: 123,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +133,7 @@ describe('compact configuration and defaults', () => {
|
||||
expect(resolved).toEqual({
|
||||
thresholdRatio: 0.8,
|
||||
retainTokens: 160,
|
||||
summarizationProvider: '',
|
||||
summarizationModel: '',
|
||||
maxTokens: 8192,
|
||||
compactionRetries: 1,
|
||||
@@ -158,7 +167,10 @@ describe('compact configuration and defaults', () => {
|
||||
[{ maxTokens: 0 }, /maxTokens/],
|
||||
[{ compactionRetries: -1 }, /compactionRetries/],
|
||||
[{ auto: 'yes' }, /auto must be a boolean/],
|
||||
[{ summarizationProvider: 1 }, /summarizationProvider must be a string/],
|
||||
[{ summarizationModel: 1 }, /summarizationModel must be a string/],
|
||||
[{ summarizationProvider: MODEL }, /must both be set or both be empty/],
|
||||
[{ summarizationModel: MODEL }, /must both be set or both be empty/],
|
||||
[{ thresholdRatio: 0 }, /number in \(0, 1\]/],
|
||||
[{ thresholdRatio: 1.1 }, /number in \(0, 1\]/],
|
||||
[{ retainTokens: -1 }, /non-negative integer/],
|
||||
@@ -232,13 +244,14 @@ describe('pressure measurement and retention', () => {
|
||||
}, ctx)
|
||||
const session = conversation(4)
|
||||
session.append('request/header', {
|
||||
header: { config: { model: 'actual' } },
|
||||
header: { config: { provider: 'actual', model: 'actual' } },
|
||||
reason: 'initial',
|
||||
})
|
||||
const measure = vi.spyOn(ctx.tokenMeter, 'measure')
|
||||
|
||||
const result = await compactIfNeeded(compact, session, 'fallback')
|
||||
expect(result).not.toBeNull()
|
||||
expect(measure.mock.calls[0]?.[1]?.config.provider).toBe('actual')
|
||||
expect(measure.mock.calls[0]?.[1]?.config.model).toBe('actual')
|
||||
})
|
||||
|
||||
@@ -315,6 +328,7 @@ describe('pressure measurement and retention', () => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('assistant/message', {
|
||||
provenance: { provider: MODEL, model: MODEL },
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [{ type: 'tool-call', id: callId, name: 'read', arguments: '{}' }],
|
||||
@@ -375,6 +389,7 @@ describe('compaction region transaction', () => {
|
||||
expect(summary?.data).toMatchObject({
|
||||
shadowedSeqs: result.shadowedSeqs,
|
||||
shadowedTokenCount: result.shadowedTokenCount,
|
||||
provider: 'summary-provider',
|
||||
model: 'summary-model',
|
||||
maxTokens: 123,
|
||||
})
|
||||
@@ -526,7 +541,7 @@ describe('compaction region transaction', () => {
|
||||
const session = conversation(2)
|
||||
compact.mutateDuringSummary = () => {
|
||||
session.append('request/header', {
|
||||
header: { config: { model: MODEL } },
|
||||
header: { config: { provider: MODEL, model: MODEL } },
|
||||
reason: 'initial',
|
||||
})
|
||||
}
|
||||
@@ -621,6 +636,7 @@ describe('default one-shot summarizer', () => {
|
||||
{ type: 'tool-call', id: CallId('unexpected'), name: 'x', arguments: '{}' },
|
||||
], undefined, MODEL, {
|
||||
auto: false,
|
||||
summarizationProvider: MODEL,
|
||||
summarizationModel: MODEL,
|
||||
maxTokens: 321,
|
||||
})
|
||||
@@ -629,10 +645,12 @@ describe('default one-shot summarizer', () => {
|
||||
|
||||
expect(output).toEqual({
|
||||
summary: [{ type: 'text', text: 'public summary' }],
|
||||
provider: MODEL,
|
||||
model: MODEL,
|
||||
maxTokens: 321,
|
||||
})
|
||||
expect(adapter.lastOptions).toMatchObject({
|
||||
provider: MODEL,
|
||||
model: MODEL,
|
||||
maxTokens: 321,
|
||||
signal: SIGNAL,
|
||||
@@ -641,25 +659,27 @@ describe('default one-shot summarizer', () => {
|
||||
expect(adapter.lastOptions?.system).toContain('## Primary Request and Intent')
|
||||
})
|
||||
|
||||
it('resolves latest routed model before AgentOptions.model', async () => {
|
||||
it('resolves the latest routed provider/model before the AgentOptions pair', async () => {
|
||||
const { adapter, compact } = await summarizerHarness([{ type: 'text', text: 'summary' }], undefined, 'routed')
|
||||
const session = conversation(1)
|
||||
session.append('request/header', {
|
||||
header: { config: { model: 'routed' } },
|
||||
header: { config: { provider: 'routed', model: 'routed' } },
|
||||
reason: 'initial',
|
||||
})
|
||||
const output = await compact.summarize('history', agent(session, 'fallback'))
|
||||
expect(output.provider).toBe('routed')
|
||||
expect(output.model).toBe('routed')
|
||||
expect(adapter.lastOptions?.provider).toBe('routed')
|
||||
expect(adapter.lastOptions?.model).toBe('routed')
|
||||
})
|
||||
|
||||
it('fails clearly when no summarization model can be resolved', async () => {
|
||||
it('fails clearly when no complete summarization target can be resolved', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
void new TokenMeterService(ctx)
|
||||
const compact = new BasicCompactService(ctx, { auto: false })
|
||||
await expect(compact.summarize('history', agent(new Session(SessionId('model-less')))))
|
||||
.rejects.toThrow(/no model available for summarization/)
|
||||
.rejects.toThrow(/no provider\/model available for summarization/)
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -20,8 +20,12 @@ import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
|
||||
*/
|
||||
|
||||
class ReproCompactService extends BasicCompactService {
|
||||
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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +98,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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user