Merge latest origin/master into compact-post-step-overflow-recovery

# Conflicts:
#	docs/agent-lifecycle.md
#	docs/architecture.md
#	docs/cookbook/extension-cookbook.i18n.yaml
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/rfc/INDEX.md
#	docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md
#	docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.i18n.yaml
#	docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.md
#	docs/rfc/implemented/architecture/2026-07-15-replay-token-meter-service.zh.md
#	docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md
#	docs/rfc/implemented/feature/2026-07-07-session-prefix.md
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/src/config.ts
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/src/summarizer.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/compact/compact-basic/tests/compact-loop-repro.spec.ts
#	packages/compact/compact/README.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/cancel.spec.ts
#	packages/llm/llm-deepseek/src/adapter.ts
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/src/stream.ts
#	packages/llm/llm-pi-ai/tests/convert.spec.ts
#	packages/llm/llm/README.md
#	packages/llm/llm/src/index.ts
#	packages/llm/llm/tests/service.spec.ts
#	scripts/gen-doc-graphs.ts
This commit is contained in:
Tianyi Cui
2026-07-19 12:06:23 +08:00
814 files changed
+42348 -10067

No files matched your search

+12 -2
View File
@@ -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',
@@ -31,8 +32,8 @@ 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, maxOverflowRetries, auto)',
+ '(allowed: thresholdRatio, retainTokens, summarizationProvider, summarizationModel, '
+ 'maxTokens, compactionRetries, maxOverflowRetries, auto)',
)
}
}
@@ -55,6 +56,7 @@ export function resolveConfig(
const resolved: ResolvedConfig = {
thresholdRatio,
retainTokens,
summarizationProvider: config.summarizationProvider ?? '',
summarizationModel: config.summarizationModel ?? '',
maxTokens: config.maxTokens ?? 8192,
compactionRetries: config.compactionRetries ?? 1,
@@ -73,9 +75,17 @@ export function resolveConfig(
assertPositiveInteger('maxTokens', resolved.maxTokens)
assertNonNegativeInteger('compactionRetries', resolved.compactionRetries)
assertNonNegativeInteger('maxOverflowRetries', resolved.maxOverflowRetries)
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')
}
+8 -13
View File
@@ -20,7 +20,6 @@ import type {
ResolvedConfig,
} from './types.ts'
export { resolveConfig } from './config.ts'
export type {
BasicCompactConfig,
ResolvedConfig,
@@ -46,6 +45,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),
@@ -126,11 +126,11 @@ export class BasicCompactService extends CompactService {
* @param signal - optional cancellation forwarded to the adapter.
* @returns safe text summary blocks and exact auxiliary-call provenance.
*/
async summarize(
protected async summarize(
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)
}
@@ -156,7 +156,7 @@ export class BasicCompactService extends CompactService {
const measurement = meter.measure(agent.session)
const range = selectCompactableRange(agent.session, measurement, 0)
if (range === null) return null
return this.compactRegion(agent.session, range.start, range.end, agent, signal)
return this.compactRegion(range.start, range.end, agent, signal)
}
const threshold = Math.floor(meter.contextWindow * this.config.thresholdRatio)
@@ -172,7 +172,7 @@ export class BasicCompactService extends CompactService {
/* v8 ignore next -- paired with the defensive post-success branch above. */
break
}
result = await this.compactRegion(agent.session, range.start, range.end, agent, signal)
result = await this.compactRegion(range.start, range.end, agent, signal)
measurement = meter.measure(agent.session)
if (measurement.totalTokens < threshold) return result
}
@@ -184,10 +184,8 @@ export class BasicCompactService extends CompactService {
}
/**
* Compact one inclusive positional surface range using the effective
* token meter for all retention and shrink pricing. Reject an agent that does
* not own the exact target before any mutation.
* @param session - session whose surface is mutated; must equal `agent.session`.
* Compact one inclusive positional range from the agent-owned surface using
* the effective token meter for all retention and shrink pricing.
* @param start - inclusive first surface-node seq.
* @param end - inclusive last surface-node seq.
* @param agent - owner of the target session, used by the summarizer.
@@ -195,15 +193,12 @@ export class BasicCompactService extends CompactService {
* @returns the successful durable compaction result.
*/
override async compactRegion(
session: Session,
start: number,
end: number,
agent: Agent,
signal?: AbortSignal,
): Promise<CompactionResult> {
if (session !== agent.session) {
throw new Error('compactRegion: agent.session must be the exact target session')
}
const session = agent.session
return compactSurfaceRegion({
meter: this.ctx.tokenMeter,
summarize: (text, owner, abort) => this.summarize(text, owner, abort),
+7 -6
View File
@@ -39,7 +39,7 @@ export function selectCompactableRange(
const surfaceNodes = session.surface.nodes
if (surfaceNodes.length !== pricedNodes.length
|| surfaceNodes.some((node, index) => node.seq !== pricedNodes[index]?.seq)) {
|| surfaceNodes.some((seq, index) => seq !== pricedNodes[index]?.seq)) {
throw new Error('compaction: token-meter surface does not match the current session surface')
}
@@ -64,7 +64,7 @@ export function selectCompactableRange(
const first = surfaceNodes[0]!
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const cutoff = surfaceNodes[keepFromIdx - 1]!
return { start: first.seq, end: cutoff.seq }
return { start: first, end: cutoff }
}
/**
@@ -86,8 +86,8 @@ export async function compactSurfaceRegion(
signal?: AbortSignal,
): Promise<CompactionResult> {
const nodes = session.surface.nodes
const startIdx = nodes.findIndex(node => node.seq === start)
const endIdx = nodes.findIndex(node => node.seq === end)
const startIdx = nodes.indexOf(start)
const endIdx = nodes.indexOf(end)
if (startIdx === -1) throw new Error(`compactRegion: start seq ${start} not found in surface`)
if (endIdx === -1) throw new Error(`compactRegion: end seq ${end} not found in surface`)
if (startIdx > endIdx) {
@@ -110,7 +110,7 @@ export async function compactSurfaceRegion(
throw new Error('compactRegion: no open turn — compaction events must be enclosed in a turn')
}
const shadowedSeqs = nodes.slice(startIdx, endIdx + 1).map(node => node.seq)
const shadowedSeqs = nodes.slice(startIdx, endIdx + 1)
const startEvent = session.append('compact/start', { turn: tail.turn })
try {
// Capture after the lock event so any later durable append, including a
@@ -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: options.model, maxTokens: config.maxTokens }
return {
summary,
provider: options.provider,
model: options.model,
maxTokens: config.maxTokens,
}
}
/**
+4 -1
View File
@@ -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
@@ -26,6 +28,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