mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/sandbox-acp-agent/cordis.yml # examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl # packages/compact/compact-basic/README.md # packages/compact/compact-basic/src/index.ts # packages/compact/compact-basic/tests/compact-basic.spec.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/agent-loop/tests/properties.spec.ts # packages/core/session/README.md # packages/core/session/src/types.ts # packages/core/session/tests/derived-cache.spec.ts # packages/llm/llm-deepseek/src/index.ts # packages/llm/llm-pi-ai/README.md # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm-pi-ai/src/convert.ts # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm/README.md # packages/llm/llm/src/call-config.ts # packages/llm/llm/src/index.ts # packages/ui/acp-agent/src/index.ts # packages/ui/acp/tests/harness.ts # packages/ui/jsonrpc/README.md # packages/ui/jsonrpc/src/server.ts # packages/ui/stdio-agent/README.md # packages/ui/stdio-agent/src/index.ts # python/sdk/README.i18n.yaml
103 lines
4.6 KiB
TypeScript
103 lines
4.6 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import LlmService from '@deepseek-ai/dsh-llm'
|
|
import SessionStore from '@deepseek-ai/dsh-session'
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
|
|
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
|
|
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
|
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
|
|
|
|
/**
|
|
* With-key proof that log-derived requests translate into real provider cache hits: a
|
|
* multi-step tool turn (plus a follow-up turn) against the live DeepSeek API must report
|
|
* `cacheReadTokens > 0` on every request after the first — the adapter maps the provider's
|
|
* `prompt_cache_hit_tokens`, and the per-step usage recorded on `assistant/message` events is
|
|
* the production observable for cache behavior (the reconstructability RFC's measurement
|
|
* layer: prefix stability is corollary #1). Mocks establish append-extension;
|
|
* this key-gated test establishes a real provider cache hit.
|
|
*/
|
|
|
|
// Long enough that the shared request prefix comfortably spans the provider's
|
|
// cache-block granularity (64 tokens) from the very first request.
|
|
const SYSTEM = 'You are a terse coding assistant used in an automated cache test. '
|
|
+ 'Always follow instructions literally and exactly. When the user asks you to look '
|
|
+ 'something up, call the lookup tool with the requested key and wait for its result '
|
|
+ 'before answering. Never invent a value the tool has not returned. After the tool '
|
|
+ 'returns, answer with a single short sentence that repeats the returned value '
|
|
+ 'verbatim. Do not add explanations, do not use markdown, do not ask follow-up '
|
|
+ 'questions. If the user asks anything else, answer in one short sentence.'
|
|
|
|
let ctx: Context | undefined
|
|
|
|
afterEach(async () => {
|
|
await ctx?.fiber.dispose()
|
|
ctx = undefined
|
|
})
|
|
|
|
async function loopHarness(): Promise<Context> {
|
|
const created = new Context()
|
|
await created.plugin(LlmService)
|
|
await created.plugin(SessionStore)
|
|
await created.plugin(SystemPrompt, { persona: SYSTEM })
|
|
await created.plugin(ToolRegistry)
|
|
await created.plugin(AgentRegistry)
|
|
await created.plugin(AgentLoop, { agents: [] })
|
|
await created.plugin(LlmDeepSeek)
|
|
created.tools.register(defineTool({
|
|
name: 'lookup',
|
|
description: 'Look up the stored value for a key.',
|
|
parameters: { key: { type: 'string', description: 'The key to look up.' } },
|
|
async execute(args) {
|
|
return [{ type: 'text', text: `value(${String(args.key)}) = azure-falcon-42` }]
|
|
},
|
|
}))
|
|
return created
|
|
}
|
|
|
|
function waitForIdle(context: Context, agent: Agent): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
const dispose = context.on('agent/status', (subject, status) => {
|
|
if (subject === agent && status === 'idle') {
|
|
dispose()
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
describe.skipIf(!process.env.DEEPSEEK_API_KEY)('log-derived request cache hits (real API)', () => {
|
|
it('every request after the first hits the provider prefix cache', async () => {
|
|
ctx = await loopHarness()
|
|
const agent = ctx.agentLoop.create(AgentId('cache-e2e'), { provider: 'deepseek', model: 'deepseek-v4-flash' })
|
|
|
|
// Turn 1: forces a tool call → at least two steps (two model requests).
|
|
agent.send([{ type: 'text', text: 'Look up the key "deploy-color" with the lookup tool and tell me the value.' }])
|
|
await waitForIdle(ctx, agent)
|
|
// Turn 2: a follow-up over the same (longer) prefix.
|
|
agent.send([{ type: 'text', text: 'Thanks. Repeat that value one more time.' }])
|
|
await waitForIdle(ctx, agent)
|
|
|
|
const usages = [...agent.session.events]
|
|
.filter(e => e.type === 'assistant/message')
|
|
.map(e => e.data.usage)
|
|
expect(usages.length).toBeGreaterThanOrEqual(3) // 2 steps in turn 1 + ≥1 in turn 2
|
|
for (const usage of usages) expect(usage).toBeDefined()
|
|
|
|
// The first request has nothing to hit; every later one shares its
|
|
// predecessor as a byte-identical prefix, so the provider must report
|
|
// cached prompt tokens (prompt_cache_hit_tokens → cacheReadTokens).
|
|
for (const usage of usages.slice(1)) {
|
|
expect(usage!.cacheReadTokens ?? 0).toBeGreaterThan(0)
|
|
}
|
|
|
|
// World-verification of the conversation itself: the tool value made it
|
|
// through the loop into the final answer.
|
|
const finalText = agent.session.deriveMessages().at(-1)!.content
|
|
.filter(block => block.type === 'text')
|
|
.map(block => block.text)
|
|
.join('')
|
|
expect(finalText).toContain('azure-falcon-42')
|
|
}, 180_000)
|
|
})
|