mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
@@ -11,9 +11,16 @@ import {
|
||||
} from '@deepseek-ai/dsh-compact-basic/src/config.ts'
|
||||
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type { ContentBlock, GenerateOptions, LlmModelContext, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
ContentBlock,
|
||||
GenerateOptions,
|
||||
LlmFailure,
|
||||
LlmModelContext,
|
||||
StreamChunk,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
const SIGNAL = new AbortController().signal
|
||||
@@ -132,6 +139,43 @@ function toolConversation(): Session {
|
||||
return session
|
||||
}
|
||||
|
||||
/** One closed routed tool step followed by an open turn for rewrite events. */
|
||||
function oversizedToolResult(chars = 3_000, withCompactablePrompt = false): Session {
|
||||
const session = new Session(SessionId(`oversized-tool-${chars}`))
|
||||
const callId = CallId('oversized')
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
if (withCompactablePrompt) {
|
||||
session.append('user/message', {
|
||||
content: [{ type: 'text', text: 'older history '.repeat(200) }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
session.append('request/header', {
|
||||
header: { config: { provider: MODEL, model: MODEL } },
|
||||
reason: 'initial',
|
||||
})
|
||||
session.append('assistant/message', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
|
||||
provenance: { provider: MODEL, model: MODEL },
|
||||
}, { surfaceOp: 'append' })
|
||||
session.append('tool/call', { turn: 1, step: 1, callId, name: 'bash', arguments: '{}' })
|
||||
session.append('tool/result', {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
callId,
|
||||
content: [{ type: 'text', text: 'X'.repeat(chars) }],
|
||||
isError: false,
|
||||
meta: { presentation: 'preserved' },
|
||||
}, { 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' } } })
|
||||
return session
|
||||
}
|
||||
|
||||
class TestCompactService extends BasicCompactService {
|
||||
summary: ContentBlock[] = [{ type: 'text', text: 'small checkpoint' }]
|
||||
summaryProvider = 'summary-provider'
|
||||
@@ -579,6 +623,78 @@ describe('pressure measurement and retention', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('optional model-free tool-result pruning', () => {
|
||||
const pruneConfig = { thresholdChars: 100, headChars: 20, tailChars: 10 }
|
||||
|
||||
it('does not prune a below-pressure session opportunistically', async () => {
|
||||
const ctx = createContext(10_000)
|
||||
const prune = new ToolResultPruneService(ctx, pruneConfig)
|
||||
const compact = new TestCompactService(ctx, {
|
||||
auto: false,
|
||||
thresholdRatio: 0.8,
|
||||
retainTokens: 100,
|
||||
})
|
||||
const session = oversizedToolResult()
|
||||
const pruneSession = vi.spyOn(prune, 'pruneSession')
|
||||
|
||||
expect(await compactIfNeeded(compact, session)).toBeNull()
|
||||
expect(pruneSession).not.toHaveBeenCalled()
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
expect(session.surface.replaceGeneration).toBe(0)
|
||||
})
|
||||
|
||||
it('skips LLM summarization when pruning alone clears pressure', async () => {
|
||||
const ctx = createContext(1_000)
|
||||
void new ToolResultPruneService(ctx, pruneConfig)
|
||||
const compact = new TestCompactService(ctx, {
|
||||
auto: false,
|
||||
thresholdRatio: 0.5,
|
||||
retainTokens: 50,
|
||||
})
|
||||
const session = oversizedToolResult()
|
||||
|
||||
expect(ctx.tokenMeter.measure(session).totalTokens).toBeGreaterThanOrEqual(500)
|
||||
expect(await compactIfNeeded(compact, session)).toBeNull()
|
||||
expect(ctx.tokenMeter.measure(session).totalTokens).toBeLessThan(500)
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
expect(session.surface.replaceGeneration).toBe(1)
|
||||
})
|
||||
|
||||
it('summarizes the pruned surface when pruning is insufficient', async () => {
|
||||
const ctx = createContext(2_000)
|
||||
void new ToolResultPruneService(ctx, pruneConfig)
|
||||
const compact = new TestCompactService(ctx, {
|
||||
auto: false,
|
||||
thresholdRatio: 0.5,
|
||||
retainTokens: 50,
|
||||
})
|
||||
const session = toolConversation()
|
||||
|
||||
expect(await compactIfNeeded(compact, session)).not.toBeNull()
|
||||
expect(compact.calls).toHaveLength(1)
|
||||
expect(compact.calls[0]!.text).toContain('tool result middle pruned')
|
||||
expect(compact.calls[0]!.text).not.toContain('result 1 '.repeat(300))
|
||||
})
|
||||
|
||||
it('retains the original compact-basic behavior without the optional plugin', async () => {
|
||||
const ctx = createContext(2_000)
|
||||
const compact = new TestCompactService(ctx, {
|
||||
auto: false,
|
||||
thresholdRatio: 0.5,
|
||||
retainTokens: 50,
|
||||
})
|
||||
const session = oversizedToolResult(3_000, true)
|
||||
|
||||
expect(await compactIfNeeded(compact, session)).not.toBeNull()
|
||||
expect(compact.calls).toHaveLength(1)
|
||||
const original = session.events.find(event => event.type === 'tool/result')
|
||||
expect(original?.type === 'tool/result' && original.data.content[0])
|
||||
.toEqual({ type: 'text', text: 'X'.repeat(3_000) })
|
||||
expect(session.events.filter(event =>
|
||||
event.type === 'tool/result' && event.surfaceOp !== 'append')).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('compaction region transaction', () => {
|
||||
it('lands a framed, replayable checkpoint with exact pricing provenance', async () => {
|
||||
const compact = service()
|
||||
@@ -950,9 +1066,9 @@ describe('default one-shot summarizer', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ kind: 'error', message: 'provider failed', code: 'PROVIDER' }, 'PROVIDER', /provider failed/],
|
||||
[{ kind: 'error', message: 'opaque' }, undefined, /opaque/],
|
||||
[{ kind: 'aborted' }, 'ABORTED', /aborted/],
|
||||
[{ kind: 'error', failure: { message: 'provider failed', code: 'PROVIDER' } }, 'PROVIDER', /provider failed/],
|
||||
[{ kind: 'error', failure: { message: 'opaque', code: 'UNKNOWN' } }, 'UNKNOWN', /opaque/],
|
||||
[{ kind: 'aborted', failure: { message: 'summarization aborted', code: 'ABORTED' } }, 'ABORTED', /aborted/],
|
||||
[{ kind: 'max-tokens' }, 'MAX_TOKENS', /token cap/],
|
||||
] as Array<[(StreamChunk & { type: 'finish' })['reason'], string | undefined, RegExp]>) (
|
||||
'rejects terminal finish %#',
|
||||
@@ -990,7 +1106,9 @@ describe('automatic listener and loader composition', () => {
|
||||
signal = SIGNAL,
|
||||
next: () => Promise<{ action: 'fail' | 'retry' }> = () => Promise.resolve({ action: 'fail' }),
|
||||
): Promise<{ action: 'fail' | 'retry' }> {
|
||||
return ctx.waterfall('agent/request-error', owner, 1, 1, error, retryAttempt, signal, next)
|
||||
const failure: LlmFailure = { message: error.message, code: error.code ?? 'UNKNOWN' }
|
||||
const priorFailures = Object.freeze(Array.from({ length: retryAttempt }, () => failure))
|
||||
return ctx.waterfall('agent/request-error', owner, 1, 1, error, failure, priorFailures, signal, next)
|
||||
}
|
||||
|
||||
function overflow(message = 'provider overflow'): Error & { code: string } {
|
||||
@@ -1101,6 +1219,89 @@ describe('automatic listener and loader composition', () => {
|
||||
expect(session.surface.nodes).toContain(retainedSeq)
|
||||
})
|
||||
|
||||
it('authorizes overflow retry when pruning alone advances an indivisible surface', async () => {
|
||||
const ctx = createContext(10_000)
|
||||
void new ToolResultPruneService(ctx, {
|
||||
thresholdChars: 100,
|
||||
headChars: 20,
|
||||
tailChars: 10,
|
||||
})
|
||||
const compact = new TestCompactService(ctx, {
|
||||
thresholdRatio: 1,
|
||||
retainTokens: 900,
|
||||
})
|
||||
const session = oversizedToolResult()
|
||||
|
||||
expect(await recover(ctx, agent(session, MODEL), overflow())).toEqual({ action: 'retry' })
|
||||
expect(session.surface.replaceGeneration).toBe(1)
|
||||
expect(session.events.some(event => event.type === 'compact/summary')).toBe(false)
|
||||
expect(compact.calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('continues overflow recovery with summarization on the pruned surface', async () => {
|
||||
const ctx = createContext(10_000)
|
||||
void new ToolResultPruneService(ctx, {
|
||||
thresholdChars: 100,
|
||||
headChars: 20,
|
||||
tailChars: 10,
|
||||
})
|
||||
const compact = new TestCompactService(ctx, {
|
||||
thresholdRatio: 1,
|
||||
retainTokens: 900,
|
||||
})
|
||||
const session = toolConversation()
|
||||
|
||||
expect(await recover(ctx, agent(session, MODEL), overflow())).toEqual({ action: 'retry' })
|
||||
expect(session.events.some(event => event.type === 'compact/summary')).toBe(true)
|
||||
expect(compact.calls).toHaveLength(1)
|
||||
expect(compact.calls[0]!.text).toContain('tool result middle pruned')
|
||||
})
|
||||
|
||||
it('retries from a durable prune when later overflow summarization throws', async () => {
|
||||
const ctx = createContext(10_000)
|
||||
const warnings: string[] = []
|
||||
ctx.logger.warn = ((message: string) => void warnings.push(message)) as typeof ctx.logger.warn
|
||||
void new ToolResultPruneService(ctx, {
|
||||
thresholdChars: 100,
|
||||
headChars: 20,
|
||||
tailChars: 10,
|
||||
})
|
||||
const compact = new TestCompactService(ctx, {
|
||||
thresholdRatio: 1,
|
||||
retainTokens: 900,
|
||||
})
|
||||
compact.error = new Error('summary unavailable after prune')
|
||||
const session = oversizedToolResult(3_000, true)
|
||||
|
||||
expect(await recover(ctx, agent(session, MODEL), overflow())).toEqual({ action: 'retry' })
|
||||
expect(session.surface.replaceGeneration).toBe(1)
|
||||
expect(session.events.filter(event => event.type === 'tool/result')).toHaveLength(2)
|
||||
expect(session.events.findLast(event => event.type === 'compact/end')?.data)
|
||||
.toMatchObject({ error: 'summary unavailable after prune' })
|
||||
expect(warnings).toContainEqual(expect.stringContaining('retrying from the replacement surface'))
|
||||
})
|
||||
|
||||
it('lets cancellation win when summary throws after a durable prune', async () => {
|
||||
const ctx = createContext(10_000)
|
||||
const controller = new AbortController()
|
||||
void new ToolResultPruneService(ctx, {
|
||||
thresholdChars: 100,
|
||||
headChars: 20,
|
||||
tailChars: 10,
|
||||
})
|
||||
const compact = new TestCompactService(ctx, {
|
||||
thresholdRatio: 1,
|
||||
retainTokens: 900,
|
||||
})
|
||||
compact.mutateDuringSummary = () => { controller.abort('cancelled during summary') }
|
||||
compact.error = new Error('summary cancelled after prune')
|
||||
const session = oversizedToolResult(3_000, true)
|
||||
|
||||
expect(await recover(ctx, agent(session, MODEL), overflow(), 0, controller.signal))
|
||||
.toEqual({ action: 'fail' })
|
||||
expect(session.surface.replaceGeneration).toBe(1)
|
||||
})
|
||||
|
||||
it('preserves the newest whole tool-call/result pair during forced overflow compaction', async () => {
|
||||
const ctx = createContext()
|
||||
void new TestCompactService(ctx, {
|
||||
@@ -1241,6 +1442,23 @@ describe('automatic listener and loader composition', () => {
|
||||
expect(compactSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('applies the routed model override to the overflow retry cap', async () => {
|
||||
const ctx = createContext()
|
||||
const compact = new TestCompactService(ctx, {
|
||||
maxOverflowRetries: 2,
|
||||
modelPolicies: [{
|
||||
provider: MODEL,
|
||||
model: MODEL,
|
||||
maxOverflowRetries: 1,
|
||||
}],
|
||||
})
|
||||
const compactSpy = vi.spyOn(compact, 'compactIfNeeded')
|
||||
|
||||
expect(await recover(ctx, agent(conversation(3), MODEL), overflow(), 1))
|
||||
.toEqual({ action: 'fail' })
|
||||
expect(compactSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not retry when cancellation lands during an awaited compaction', async () => {
|
||||
const ctx = createContext()
|
||||
const compact = new TestCompactService(ctx)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
|
||||
import * as Invariants from '@deepseek-ai/dsh-invariants'
|
||||
import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import * as LlmRetry from '@deepseek-ai/dsh-llm-retry'
|
||||
import { SessionId, type SurfaceEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/**
|
||||
@@ -65,7 +66,10 @@ class OverflowRecoveryAdapter extends LlmAdapter {
|
||||
readonly conversationRequests: GenerateOptions[] = []
|
||||
readonly summaryRequests: GenerateOptions[] = []
|
||||
|
||||
constructor(private readonly delivery: 'thrown' | 'in-band') {
|
||||
constructor(
|
||||
private readonly delivery: 'thrown' | 'in-band',
|
||||
private readonly transientAfterOverflow = false,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
@@ -91,12 +95,17 @@ class OverflowRecoveryAdapter extends LlmAdapter {
|
||||
type: 'finish',
|
||||
reason: {
|
||||
kind: 'error',
|
||||
message: 'request too large for model context',
|
||||
code: CONTEXT_WINDOW_EXCEEDED_CODE,
|
||||
failure: {
|
||||
message: 'request too large for model context',
|
||||
code: CONTEXT_WINDOW_EXCEEDED_CODE,
|
||||
},
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
if (this.transientAfterOverflow && this.conversationRequests.length === 2) {
|
||||
throw new LlmError('temporary provider outage', 'SERVER')
|
||||
}
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text: 'recovered' } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
@@ -142,6 +151,29 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
function seedOverflowHistory(agent: Agent): void {
|
||||
for (let turn = 1; turn <= 2; turn += 1) {
|
||||
const sentinel = turn === 1 ? 'OLD HISTORY SENTINEL' : 'RECENT HISTORY'
|
||||
agent.session.append('turn/start', {
|
||||
turn,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})
|
||||
agent.session.append('user/message', {
|
||||
content: [{ type: 'text', text: `${sentinel} ${'old context '.repeat(200)}` }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
agent.session.append('step/start', { turn, step: 1 })
|
||||
agent.session.append('assistant/message', {
|
||||
provenance: { provider: 'mock', model: 'mock' },
|
||||
turn,
|
||||
step: 1,
|
||||
content: [{ type: 'text', text: `historical response ${turn} ${'detail '.repeat(200)}` }],
|
||||
}, { surfaceOp: 'append' })
|
||||
agent.session.append('step/end', { turn, step: 1 })
|
||||
agent.session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||
}
|
||||
}
|
||||
|
||||
describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () => {
|
||||
it('uses the model actually routed by agent/request for post-step pressure', async () => {
|
||||
const { ctx } = await harness(8)
|
||||
@@ -249,26 +281,7 @@ describe('context-overflow recovery across the real loop and compact-basic', ()
|
||||
provider: 'unconfigured-agent-fallback',
|
||||
model: 'unconfigured-agent-fallback',
|
||||
})
|
||||
for (let turn = 1; turn <= 2; turn += 1) {
|
||||
const sentinel = turn === 1 ? 'OLD HISTORY SENTINEL' : 'RECENT HISTORY'
|
||||
agent.session.append('turn/start', {
|
||||
turn,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})
|
||||
agent.session.append('user/message', {
|
||||
content: [{ type: 'text', text: `${sentinel} ${'old context '.repeat(200)}` }],
|
||||
source: { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
agent.session.append('step/start', { turn, step: 1 })
|
||||
agent.session.append('assistant/message', {
|
||||
provenance: { provider: 'mock', model: 'mock' },
|
||||
turn,
|
||||
step: 1,
|
||||
content: [{ type: 'text', text: `historical response ${turn} ${'detail '.repeat(200)}` }],
|
||||
}, { surfaceOp: 'append' })
|
||||
agent.session.append('step/end', { turn, step: 1 })
|
||||
agent.session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||
}
|
||||
seedOverflowHistory(agent)
|
||||
|
||||
agent.send([{ type: 'text', text: 'continue from history' }])
|
||||
await agent.whenIdle()
|
||||
@@ -307,4 +320,47 @@ describe('context-overflow recovery across the real loop and compact-basic', ()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
it('keeps context-overflow and transient retry budgets independent in one sequence', async () => {
|
||||
const ctx = new Context()
|
||||
const adapter = new OverflowRecoveryAdapter('thrown', true)
|
||||
await mountAgentLoopTestDependencies(ctx)
|
||||
await ctx.plugin(Invariants)
|
||||
await ctx.plugin(LlmRetry, {
|
||||
maxTransientRetries: 1,
|
||||
initialDelayMs: 1,
|
||||
maxDelayMs: 1,
|
||||
jitterRatio: 0,
|
||||
})
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(TokenMeterService)
|
||||
ctx.llm.registerAdapter(['mock'], adapter)
|
||||
await ctx.plugin(BasicCompactService, {
|
||||
thresholdRatio: 1,
|
||||
retainTokens: 100,
|
||||
maxTokens: 64,
|
||||
compactionRetries: 0,
|
||||
maxOverflowRetries: 1,
|
||||
})
|
||||
|
||||
try {
|
||||
const agent = ctx.agentLoop.create(SessionId('alternating-recovery'), { provider: 'mock', model: 'mock' })
|
||||
seedOverflowHistory(agent)
|
||||
agent.send([{ type: 'text', text: 'continue from history' }])
|
||||
await agent.whenIdle()
|
||||
|
||||
expect(adapter.conversationRequests).toHaveLength(3)
|
||||
expect(adapter.summaryRequests).toHaveLength(1)
|
||||
expect(agent.session.events.filter(event => event.type === 'llm/retry').map(event => event.data))
|
||||
.toEqual([expect.objectContaining({ step: 2, retry: 1, failure: { message: 'temporary provider outage', code: 'SERVER' } })])
|
||||
expect(agent.session.events.filter(event => event.type === 'step/start').slice(-3).map(event => event.data.step))
|
||||
.toEqual([1, 2, 3])
|
||||
expect(agent.session.events.at(-1)).toMatchObject({
|
||||
type: 'turn/end',
|
||||
data: { reason: { kind: 'completed' } },
|
||||
})
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import Include from '@cordisjs/plugin-include'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import BasicCompactService from '@deepseek-ai/dsh-compact-basic'
|
||||
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
|
||||
let root: string | undefined
|
||||
let context: Context | undefined
|
||||
@@ -32,6 +33,7 @@ async function loadYaml(lines: readonly string[]): Promise<Context> {
|
||||
const modules = new Map<string, unknown>([
|
||||
['@deepseek-ai/dsh-llm', LlmService],
|
||||
['@deepseek-ai/dsh-token-meter', TokenMeterService],
|
||||
['@deepseek-ai/dsh-compact-tool-result-prune', ToolResultPruneService],
|
||||
['@deepseek-ai/dsh-compact-basic', BasicCompactService],
|
||||
])
|
||||
context.loader.internal = {
|
||||
@@ -50,10 +52,15 @@ async function loadYaml(lines: readonly string[]): Promise<Context> {
|
||||
}
|
||||
|
||||
describe('real Loader composition', () => {
|
||||
it('loads the flat token-meter and compact-basic YAML shape', async () => {
|
||||
it('loads the shipped token-meter, pruning, and compact-basic YAML order', async () => {
|
||||
const loaded = await loadYaml([
|
||||
"- name: '@deepseek-ai/dsh-llm'",
|
||||
"- name: '@deepseek-ai/dsh-token-meter'",
|
||||
"- name: '@deepseek-ai/dsh-compact-tool-result-prune'",
|
||||
' config:',
|
||||
' thresholdChars: 100',
|
||||
' headChars: 20',
|
||||
' tailChars: 10',
|
||||
"- name: '@deepseek-ai/dsh-compact-basic'",
|
||||
' config:',
|
||||
' thresholdRatio: 0.5',
|
||||
@@ -65,6 +72,7 @@ describe('real Loader composition', () => {
|
||||
.filter(entry => entry.fiber === undefined && !entry.disabled)
|
||||
.map(entry => entry.options.name)
|
||||
expect(unloaded).toEqual([])
|
||||
expect(loaded.get('toolResultPrune')).toBeInstanceOf(ToolResultPruneService)
|
||||
expect(loaded.get('compact')).toBeInstanceOf(BasicCompactService)
|
||||
expect((loaded.compact as BasicCompactService).config).toMatchObject({
|
||||
thresholdRatio: 0.5,
|
||||
|
||||
Reference in New Issue
Block a user