Merge remote-tracking branch 'origin/master' into worktree/routed-model-compaction-policy
# Conflicts: # .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.i18n.yaml # .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.md # .agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/event-producer-consumer.md # examples/headless-agent/tests/harness.ts # examples/repl-agent/cordis.yml # packages/compact/compact-basic/README.md # packages/compact/compact-basic/src/index.ts # packages/compact/compact-basic/tests/compact-basic.spec.ts # packages/compact/compact-basic/tests/loader-composition.spec.ts # packages/cordis/tool-cordis/src/api-catalog.ts # packages/llm/README.md # packages/llm/llm-deepseek/src/adapter.ts # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm/README.md # packages/llm/llm/src/index.ts # scripts/gen-cordis-catalog.ts # website/zh-CN/api/harness/events.md # website/zh-CN/api/harness/llm.md # website/zh-CN/api/harness/token-meter.md # website/zh-CN/guide/config.md
This commit is contained in:
@@ -12,6 +12,8 @@ import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import { CONTEXT_WINDOW_EXCEEDED_CODE, assertNever } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, LlmCallConfig } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
// Type-only: makes the optional sibling service available to `ctx.get()`.
|
||||
import type {} from '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
import {
|
||||
resolveCompactSpec,
|
||||
resolveConfig,
|
||||
@@ -150,29 +152,54 @@ export class BasicCompactService extends CompactService {
|
||||
}
|
||||
})
|
||||
|
||||
ctx.on('agent/request-error', async (agent, _turn, _step, error, retryAttempt, signal, next) => {
|
||||
if (error.code !== CONTEXT_WINDOW_EXCEEDED_CODE || signal.aborted) return next()
|
||||
ctx.on('agent/request-error', async (
|
||||
agent,
|
||||
_turn,
|
||||
_step,
|
||||
_error,
|
||||
failure,
|
||||
priorFailures,
|
||||
signal,
|
||||
next,
|
||||
) => {
|
||||
const priorOverflowFailures = priorFailures.filter(
|
||||
item => item.code === CONTEXT_WINDOW_EXCEEDED_CODE,
|
||||
).length
|
||||
if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE || signal.aborted) return next()
|
||||
const target = routedTarget(agent.session)
|
||||
if (target === undefined) return next()
|
||||
const policy = resolveTargetPolicy(this.config, target)
|
||||
if (retryAttempt >= policy.maxOverflowRetries) return next()
|
||||
if (priorOverflowFailures >= policy.maxOverflowRetries) return next()
|
||||
|
||||
let generation: number
|
||||
const generation = agent.session.surface.replaceGeneration
|
||||
let result: CompactionResult | null
|
||||
try {
|
||||
generation = agent.session.surface.replaceGeneration
|
||||
result = await this.compactIfNeeded(agent, 'context-overflow', signal)
|
||||
} catch (recoveryError: unknown) {
|
||||
const message = recoveryError instanceof Error ? recoveryError.message : String(recoveryError)
|
||||
// A model-free prune can land before later summary work fails. That
|
||||
// durable reduction is sufficient retry proof; do not discard it just
|
||||
// because the optional second phase threw. Cancellation still wins.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited.
|
||||
if (!signal.aborted && agent.session.surface.replaceGeneration > generation) {
|
||||
ctx.logger.warn(
|
||||
`context-overflow compaction failed after durable surface progress: ${message}; `
|
||||
+ 'retrying from the replacement surface',
|
||||
)
|
||||
return { action: 'retry' }
|
||||
}
|
||||
ctx.logger.warn(
|
||||
`context-overflow compaction failed: ${message}; preserving the original request error`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while recovery is awaited.
|
||||
`context-overflow compaction failed: ${message}; ${signal.aborted
|
||||
? 'cancellation prevents retry'
|
||||
: 'preserving the original request error'}`,
|
||||
)
|
||||
return next()
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- signal can abort while compaction is awaited.
|
||||
if (signal.aborted || result === null
|
||||
if (signal.aborted
|
||||
|| agent.session.surface.replaceGeneration <= generation) return next()
|
||||
logResult(result, 'context overflow recovery')
|
||||
if (result !== null) logResult(result, 'context overflow recovery')
|
||||
return { action: 'retry' }
|
||||
})
|
||||
}
|
||||
@@ -205,7 +232,7 @@ export class BasicCompactService extends CompactService {
|
||||
* @param agent - agent whose latest durable routed request is measured.
|
||||
* @param trigger - normal post-step pressure or context-overflow recovery.
|
||||
* @param signal - live turn cancellation signal forwarded to summarization.
|
||||
* @returns the latest compaction result, or `null` when no check/work applies.
|
||||
* @returns the latest summary compaction result, or `null` when no summary ran.
|
||||
*/
|
||||
override async compactIfNeeded(
|
||||
agent: Agent,
|
||||
@@ -216,13 +243,10 @@ export class BasicCompactService extends CompactService {
|
||||
if (target === undefined) return null
|
||||
const policy = resolveTargetPolicy(this.config, target)
|
||||
const meter = this.ctx.tokenMeter
|
||||
let measurement = meter.measure(agent.session)
|
||||
switch (trigger) {
|
||||
case 'context-overflow': {
|
||||
const measurement = meter.measure(agent.session)
|
||||
const range = selectCompactableRange(agent.session, measurement, 0)
|
||||
if (range === null) return null
|
||||
return this.compactRegion(range.start, range.end, agent, signal)
|
||||
}
|
||||
case 'context-overflow':
|
||||
break
|
||||
case 'pressure':
|
||||
break
|
||||
/* v8 ignore next -- closed-union exhaustiveness guard */
|
||||
@@ -230,6 +254,21 @@ export class BasicCompactService extends CompactService {
|
||||
assertNever(trigger, 'compaction trigger')
|
||||
}
|
||||
|
||||
// Pruning is optional so compact-basic remains independently composable.
|
||||
// Overflow always qualifies; pressure first resolves the routed model's
|
||||
// capacity and checks its target-specific threshold.
|
||||
const prune = this.ctx.get('toolResultPrune')
|
||||
|
||||
if (trigger === 'context-overflow') {
|
||||
if (prune !== undefined) {
|
||||
prune.pruneSession(agent.session)
|
||||
measurement = meter.measure(agent.session)
|
||||
}
|
||||
const range = selectCompactableRange(agent.session, measurement, 0)
|
||||
if (range === null) return null
|
||||
return this.compactRegion(range.start, range.end, agent, signal)
|
||||
}
|
||||
|
||||
const context = await this.ctx.llm.resolveModelContext(target.provider, target.model)
|
||||
const targetKey = `${target.provider}/${target.model}`
|
||||
if (context === undefined) {
|
||||
@@ -240,7 +279,14 @@ export class BasicCompactService extends CompactService {
|
||||
)
|
||||
}
|
||||
const spec = resolveCompactSpec(policy, context.contextWindow)
|
||||
let measurement = meter.measure(agent.session)
|
||||
if (measurement.totalTokens < spec.thresholdTokens) return null
|
||||
|
||||
// Once pressure qualifies, land the model-free pass before choosing a
|
||||
// summary range, then remeasure through the singleton replay fold.
|
||||
if (prune !== undefined) {
|
||||
prune.pruneSession(agent.session)
|
||||
measurement = meter.measure(agent.session)
|
||||
}
|
||||
if (measurement.totalTokens < spec.thresholdTokens) return null
|
||||
|
||||
let result: CompactionResult | null = null
|
||||
|
||||
@@ -146,14 +146,10 @@ export function frameSummary(summary: readonly ContentBlock[]): ContentBlock[] {
|
||||
/** Map a terminal summarization finish to its fail-closed error. */
|
||||
function finishError(finish: FinishReason): Error | undefined {
|
||||
switch (finish.kind) {
|
||||
case 'error': {
|
||||
const error = new Error(finish.message) as Error & { code?: string }
|
||||
if (finish.code !== undefined) error.code = finish.code
|
||||
return error
|
||||
}
|
||||
case 'error':
|
||||
case 'aborted': {
|
||||
const error = new Error('summarization stream aborted') as Error & { code?: string }
|
||||
error.code = 'ABORTED'
|
||||
const error = new Error(finish.failure.message) as Error & { code?: string }
|
||||
error.code = finish.failure.code
|
||||
return error
|
||||
}
|
||||
case 'max-tokens': {
|
||||
|
||||
Reference in New Issue
Block a user