fix(experimental): drop unknown casts from the mods bridge and its tests

Type the mod tool's output as the answering text, read JSON into a typed
unknown, and replace the test doubles that went through unknown with a
real $ over a slow invoke, an Agent-comparable facts object, loosely typed
on() calls, and the Loader's own import path.
This commit is contained in:
Tianyi Cui committed 2026-10-02 11:27:47 +08:00
1 parent 35873604ad
commit 54fa38795f
7 files changed
+47 -33

No files matched your search

@@ -293,9 +293,9 @@ export function createHostOps(options: HostOpsOptions): OpTable<AgentBinding> {
description,
parameters: inputSchema,
output: {
// The answering hook's text, already rendered as content blocks.
schema: { type: 'array' },
render: (_args, value) => value as unknown as ContentBlock[],
// The answering hook's result text.
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: String(value) }],
},
execute() {
return Promise.reject(new Error(`${mod.name} registered ${fullName} but no tool.call hook answered it; add on('tool.call', { tool: '${fullName}' }, hook)`))
@@ -17,7 +17,6 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm'
import type { AssistantMessage, ContentBlock, ContextFormed, TokenUsage } from '@deepseek-ai/dsh-llm'
import type { UserMessage } from '@deepseek-ai/dsh-session'
import type { PostToolDecision, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import { ModsEngine } from './engine.ts'
import { createHostOps, toolCallResultOf } from './host-ops.ts'
import type { AgentBinding } from './host-ops.ts'
@@ -307,8 +306,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
return beneath
}
if (modTools.has(exec.name) && answer.isError !== true) {
const content: ContentBlock[] = [{ type: 'text', text }]
return { isError: false, value: content as unknown as JsonValue, content }
return { isError: false, value: text, content: [{ type: 'text', text }] }
}
// A built-in tool's success value must satisfy its own output schema, so a
// mod's answer in its place is reported as an error-shaped result.
@@ -124,7 +124,8 @@ function readJson(path: string): unknown {
throw new Error(`${path}: cannot read: ${messageOf(error)}`)
}
try {
return JSON.parse(text) as unknown
const parsed: unknown = JSON.parse(text)
return parsed
} catch (error: unknown) {
throw new Error(`${path}: invalid JSON: ${messageOf(error)}`)
}
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { BudgetClock, dispatch, ENGINE_ORIGIN, HookTimeoutError } from '../src/chain.ts'
import type { DispatchRequest, LoadedMod, RegisteredHook } from '../src/chain.ts'
import { createModsApi } from '../src/api.ts'
import type { AnyHook, HookMatcher, ModsApi } from '../src/types.ts'
const mod = (name: string, order = 0): LoadedMod => ({ name, version: undefined, root: '/mods/' + name, modulePath: '', options: {}, order })
@@ -212,17 +213,16 @@ describe('dispatch: failures', () => {
core: () => new Promise((resolve) => { setTimeout(() => { resolve({ result: 'slow core' }) }, 60) }),
report,
budgetMs: 25,
api: (_hook, clock) => ({
clock: {
async sleepOutside(ms: number) {
clock.pause()
await new Promise(resolve => setTimeout(resolve, ms))
clock.resume()
},
},
}) as unknown as ModsApi,
// A real `$` whose every call takes 60 ms to answer: the clock pauses for the call.
api: (hooked, clock) => createModsApi({
mod: hooked.mod,
clock,
invoke: () => new Promise((resolve) => { setTimeout(() => { resolve('contents') }, 60) }),
timers: { after: () => ({ cancel() {} }), every: () => ({ cancel() {} }) },
report: () => {},
}),
hooks: [hook(a, 'tool.call', async ($, e, next) => {
await ($ as unknown as { clock: { sleepOutside(ms: number): Promise<void> } }).clock.sleepOutside(60)
expect(await $.fs.read('notes.md')).toBe('contents')
return next(e)
})],
}))
@@ -4,6 +4,7 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { Message } from '@deepseek-ai/dsh-llm'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import CommandRuntime from '@deepseek-ai/dsh-commands'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
@@ -44,9 +45,22 @@ function setup(ctx = new Context()) {
return { ctx, ops, engine, context, call, registrations }
}
/** The members of an Agent the host ops read; a real Agent is assignable to it, which is what the assertion below relies on. */
interface AgentFacts {
readonly id: string
readonly ctx: Context
readonly options: { readonly model?: string }
readonly session: {
readonly id: string
readonly header: { readonly id: string; readonly createdAt: number; readonly cwd?: string }
requestHeader(): { readonly config: { readonly provider: string; readonly model: string } } | undefined
deriveMessages(): Message[]
}
}
/** A fake agent with the session facts the ops read; its scoped ctx is the root ctx. */
function fakeAgent(ctx: Context, overrides: { cwd?: string; model?: string; requestModel?: string } = {}): Agent {
return {
const facts: AgentFacts = {
id: 'fake',
ctx,
options: overrides.model === undefined ? {} : { model: overrides.model },
@@ -54,9 +68,10 @@ function fakeAgent(ctx: Context, overrides: { cwd?: string; model?: string; requ
id: 'fake',
header: { id: 'fake', createdAt: 1, ...overrides.cwd === undefined ? {} : { cwd: overrides.cwd } },
requestHeader: () => overrides.requestModel === undefined ? undefined : { config: { provider: 'p', model: overrides.requestModel } },
deriveMessages: () => [],
deriveMessages: (): Message[] => [],
},
} as unknown as Agent
}
return facts as Agent
}
describe('values', () => {
@@ -56,13 +56,9 @@ it('loads from cordis.yml, counts the model\'s tool calls, and answers /tally th
context.baseUrl = pathToFileURL(root).href + '/'
await context.plugin(Loader)
context.loader.builtins.include = Include
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (!modules.has(specifier)) throw new Error(`Unexpected fixture module: ${specifier}`)
return modules.get(specifier)
},
} as unknown as NonNullable<typeof context.loader.internal>
// Without a Node internal loader the Loader imports bare names through this
// test runner's module graph, which resolves workspace packages to `src`.
context.loader.internal = undefined
await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
await context.loader.await()
for (const entry of context.loader.entries()) await entry.fiber?.await()
@@ -4,7 +4,7 @@ import type { LoadedMod } from '../src/chain.ts'
import { ModsEngine, OpDenied } from '../src/engine.ts'
import { readModManifest } from '../src/manifest.ts'
import { createOn, HookRegistry, loadHooksModule } from '../src/module.ts'
import type { AnyHook, ModsApi } from '../src/types.ts'
import type { AnyHook, HookRegistration, ModsApi } from '../src/types.ts'
const FIXTURES = resolve(import.meta.dirname, 'fixtures')
const mod = (name: string, order = 0): LoadedMod => ({ name, version: undefined, root: '/mods/' + name, modulePath: '', options: {}, order })
@@ -25,15 +25,18 @@ describe('createOn', () => {
it('refuses what claude plugin validate refuses', () => {
const { on } = createOn(mod('a'))
// A JavaScript mod can pass anything, so the checks are exercised through an untyped call.
const loose = on as (...args: unknown[]) => HookRegistration
expect(() => on('tool.call', noop)).not.toThrow()
expect(() => on(42 as unknown as string, noop)).toThrow(/not a string literal/)
expect(() => loose(42, noop)).toThrow(/not a string literal/)
expect(() => on('tool.calls', noop)).toThrow(/"tool.calls" is not an event/)
expect(() => on('session.start', noop)).not.toThrow()
expect(() => on('session.start', noop)).toThrow(/on\("session.start"\) is registered twice without a matcher/)
expect(() => on('session.start', { cwd: '/x' }, noop)).not.toThrow()
expect(() => on('turn.start', 'nope' as unknown as AnyHook)).toThrow(/needs a hook function/)
expect(() => on('turn.start', 'nope' as unknown as Record<string, string>, noop)).toThrow(/matcher must be an object/)
expect(() => { on('turn.complete', noop).catch('nope' as unknown as AnyHook) }).toThrow(/needs a handler function/)
expect(() => loose('turn.start', 'nope')).toThrow(/needs a hook function/)
expect(() => loose('turn.start', 'nope', noop)).toThrow(/matcher must be an object/)
const attach = on('turn.complete', noop).catch as (handler: unknown) => void
expect(() => { attach('nope') }).toThrow(/needs a handler function/)
})
})
@@ -103,7 +106,8 @@ describe('ModsEngine', () => {
expect(await second.state.get(ref)).toEqual({ value: undefined })
e.forgetState('s1')
expect(await first.state.get(ref)).toEqual({ value: undefined })
await expect(first.state.get({ plugin: 1 as unknown as string, key: 'x' })).rejects.toThrow(/needs \{ plugin, key \} strings/)
await expect(e.invoke(owner, 'state.get', { plugin: 1, key: 'x' }, { key: 's1' }, new AbortController().signal))
.rejects.toThrow(/needs \{ plugin, key \} strings/)
const before = Date.now()
expect(await first.clock.now()).toBeGreaterThanOrEqual(before)
await first.clock.sleep(1)