Files
deepseek-harness/packages/support/loader-smoke/src/agent-turn.ts
T
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

101 lines
3.6 KiB
TypeScript

/**
* Test-only direct-agent turn driver shared by assembled Loader fixtures.
* @module @deepseek-ai/dsh-loader-smoke/agent-turn
*/
import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage, type TokenUsage } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
/** Result envelope consumed only by snapshot and composition tests. */
export interface FixtureTurnResult {
readonly type: 'result'
readonly sessionId: string
readonly output: string
readonly usage?: TokenUsage
}
/** Options for one fixture turn against exactly one configured root agent. */
export interface FixtureTurnOptions {
readonly task: string
readonly onEvent?: (sessionId: string, event: SessionEvent) => void
}
function addUsage(total: TokenUsage | undefined, step: TokenUsage): TokenUsage {
const next: TokenUsage = {
inputTokens: (total?.inputTokens ?? 0) + step.inputTokens,
outputTokens: (total?.outputTokens ?? 0) + step.outputTokens,
}
for (const key of ['cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'] as const) {
if (total?.[key] !== undefined || step[key] !== undefined) next[key] = (total?.[key] ?? 0) + (step[key] ?? 0)
}
return next
}
function assistantText(event: Extract<SessionEvent, { type: 'assistant/message' }>): string | undefined {
const blocks = event.data.message.content.filter(block => block.type === 'text')
return blocks.length === 0 ? undefined : blocks.map(block => block.text).join('')
}
function onlyRootAgent(ctx: Context): Agent {
const agents = ctx.get('agents')?.roots() ?? []
const [agent] = agents
if (agent === undefined || agents.length !== 1) {
throw new Error(`fixture turn requires exactly one top-level agent, found ${agents.length}`)
}
return agent
}
/**
* Drive one task from its durable inbox receipt through whole-agent idle.
* @param ctx - settled Loader context with exactly one configured root agent.
* @param options - task and optional canonical-event observer.
* @returns the final assistant text and accumulated model usage.
*/
export async function runFixtureTurn(ctx: Context, options: FixtureTurnOptions): Promise<FixtureTurnResult> {
const agent = onlyRootAgent(ctx)
await agent.whenIdle()
const message = createUserMessage({
content: [{ type: 'text', text: options.task }],
source: { kind: 'user' },
})
let received = false
let output = ''
const usageByStep = new Map<string, TokenUsage>()
const disposeListener = ctx.on('session/event', (session, event) => {
if (session !== agent.session) return
if (!received) {
if (event.type !== 'agent/inbox/spliced'
|| !event.data.inserted.some(inserted => inserted.id === message.id)) return
received = true
}
options.onEvent?.(session.id, event)
if (event.type === 'assistant/chunk' && event.data.chunk.type === 'usage') {
usageByStep.set(`${event.data.turn}/${event.data.step}`, event.data.chunk.usage)
}
if (event.type === 'assistant/message') {
output = assistantText(event) ?? output
if (event.data.usage !== undefined) {
usageByStep.set(`${event.data.turn}/${event.data.step}`, event.data.usage)
}
}
})
try {
agent.followup(message)
await agent.whenIdle()
} finally {
disposeListener()
}
await ctx.sessions.flush(agent.session)
const usage = [...usageByStep.values()].reduce<TokenUsage | undefined>(addUsage, undefined)
return {
type: 'result',
sessionId: agent.session.id,
output,
...usage === undefined ? {} : { usage },
}
}