Merge remote-tracking branch 'origin/master' into worktree/schedule-conversational-after

This commit is contained in:
Tianyi Cui
2026-08-11 19:05:13 +08:00
3631 changed files with 83576 additions and 36966 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import { createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
@@ -59,7 +59,7 @@ async function setup(options: SetupOptions = {}) {
return { ctx, tools: ctx.tools, systemPrompt: ctx.systemPrompt, runtime: runtime! }
}
/** Mint one production-shaped agent scope that can register scoped tool policy. */
/** Mint an agent scope configured like production that can register scoped tool policy. */
async function mintAgentScope(ctx: Context, name = 'scoped'): Promise<{ scope: Scope; agent: Agent }> {
const agent = { id: SessionId(name) } as Agent
let scope!: Scope
@@ -407,7 +407,7 @@ describe('mode-aware wire contribution', () => {
})
it('degrades the run_code flavor to TypeScript when no runtime is mounted', async () => {
// Any reader of the definition without a mounted runtime lands here; the
// Any reader of the definition without a mounted runtime uses this fallback; the
// shipped one is the tool-catalog generator, which boots the registry under
// `mode: code` and reads run_code's schema WITHOUT a runtime. peekRuntime
// returns undefined there, so the flavor getter degrades to the TS default
@@ -663,7 +663,7 @@ describe('the sub-dispatch scheduler (native concurrency contract)', () => {
expect(stages).toEqual(['post-enter:writer', 'post-exit:writer'])
})
it('run settlement drains a commit already in progress: the settle event lands inside the turn', async () => {
it('run settlement drains a commit already in progress: the settle event is appended inside the turn', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const gated = registerGated(ctx, 'safe_read', true)
const { agent, events } = fakeAgent()
@@ -921,10 +921,10 @@ describe('the run_code dispatch bridge', () => {
expect(result.content[0]).toEqual({ type: 'text', text: 'caught: deliberate failure' })
})
it('a throwing tools/code-dispatch-log listener is contained: the unshaped content is logged', async () => {
it('a throwing tools/code-dispatch-log listener is contained: the original settled content is logged', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
registerEcho(ctx)
ctx.on('tools/code-dispatch-log', () => { throw new Error('shaper exploded') })
ctx.on('tools/code-dispatch-log', () => { throw new Error('log-content listener failed') })
const { agent, events } = fakeAgent()
runtime.behavior = async (request) => {
const value = await request.bindings[0]!.functions.echo!({ value: 'x' })
@@ -1229,7 +1229,7 @@ describe('the run_code dispatch bridge', () => {
const tool = ctx.tools.get(RUN_CODE_NAME)!
expect(result.content).toEqual([{ type: 'text', text }])
// Surfaces keep the pending program title and render this durable content
// Presenters keep the pending program title and render this durable content
// through their generic fallback. Omitting a result view also prevents the
// host frame from carrying the same raw content a second time.
expect('presentResult' in tool).toBe(false)
@@ -1562,3 +1562,127 @@ describe('the run_code dispatch bridge', () => {
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
})
})
/**
* Presentation is per agent, because an agent preset composes it: one
* deployment runs a Code Mode agent beside native ones, and neither may see
* the other's catalog. The deployment `mode` is the default those agents
* shadow, not a process-wide fact.
*/
describe('per-agent presentation', () => {
it('gives one agent Code Mode while the deployment stays native', async () => {
const { ctx, systemPrompt } = await setup({ mode: 'native' })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
scope.ctx.tools.presentAs('code')
const coded = await systemPrompt.assemble({ scope: agent })
expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
expect(coded.sections.find(section => section.name === 'tools:sdk')?.text)
.toContain('echo')
// The deployment default is untouched: an agent that declared nothing —
// and the global view behind it — still sees the native catalog.
const native = await systemPrompt.assemble()
expect(native.tools.map(tool => tool.name)).toEqual(['echo'])
expect(native.sections.some(section => section.name === 'tools:sdk')).toBe(false)
})
it('inherits a STANDING preset scope\'s mode down the chain, agents beside it unaffected', async () => {
const { bindScopeParent } = await import('@deepseek-ai/dsh-scope')
const { ctx, systemPrompt } = await setup({ mode: 'native' })
registerEcho(ctx)
// The preset's standing scope declares once; the agent only PARENTS to it
// (the per-preset standing mount configuration has no per-agent declaration).
const standing = await mintAgentScope(ctx, 'preset:code-like')
standing.scope.ctx.tools.presentAs('code')
const joined = await mintAgentScope(ctx, 'joined-agent')
bindScopeParent(joined.agent, standing.agent)
const loner = await mintAgentScope(ctx, 'loner-agent')
expect(ctx.tools.get(RUN_CODE_NAME, joined.agent)).toBeDefined()
const coded = await systemPrompt.assemble({ scope: joined.agent })
expect(coded.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
// A sibling that never parented stays native, as does the global view.
expect(ctx.tools.get(RUN_CODE_NAME, loner.agent)).toBeUndefined()
const native = await systemPrompt.assemble({ scope: loner.agent })
expect(native.tools.map(tool => tool.name)).toEqual(['echo'])
})
it('keeps run_code out of a native agent\'s dispatch table', async () => {
const { ctx } = await setup({ mode: 'native' })
registerEcho(ctx)
const coded = await mintAgentScope(ctx, 'coded')
const plain = await mintAgentScope(ctx, 'plain')
coded.scope.ctx.tools.presentAs('code')
// Not merely hidden from the prompt: the transport one agent presents must
// not be dispatchable by another that never presented it.
expect(ctx.tools.get(RUN_CODE_NAME, coded.agent)).toBeDefined()
expect(ctx.tools.get(RUN_CODE_NAME, plain.agent)).toBeUndefined()
expect(ctx.tools.get(RUN_CODE_NAME)).toBeUndefined()
})
it('lets an agent opt out of a code-mode deployment', async () => {
const { ctx, systemPrompt } = await setup({ mode: 'code' })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
scope.ctx.tools.presentAs('native')
const assembly = await systemPrompt.assemble({ scope: agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['echo'])
// The deployment's global section still reaches this scope; rendering it
// empty is what keeps the opted-out agent's prompt free of an SDK.
expect(assembly.sections.find(section => section.name === 'tools:sdk')?.text).toBe('')
})
it('restores the deployment default when the agent unloads', async () => {
const { ctx, systemPrompt } = await setup({ mode: 'native' })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
const dispose = scope.ctx.tools.presentAs('code')
dispose()
const assembly = await systemPrompt.assemble({ scope: agent })
expect(assembly.tools.map(tool => tool.name)).toEqual(['echo'])
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
})
it('refuses a second declaration for the same agent', async () => {
const { ctx } = await setup({ mode: 'native' })
const { scope } = await mintAgentScope(ctx)
scope.ctx.tools.presentAs('code')
// Two answers to "which form does the model see" is a contradiction, and
// silently keeping either one would make the composition unreadable.
expect(() => scope.ctx.tools.presentAs('both'))
.toThrow('conflicts with "code" already declared')
})
it('refuses an unscoped declaration', async () => {
const { ctx } = await setup({ mode: 'native' })
expect(() => ctx.tools.presentAs('code'))
.toThrow('requires a scoped context')
})
it('reserves run_code even where no agent presents it', async () => {
const { ctx } = await setup({ mode: 'native' })
// The name must stay free under a native deployment too: an agent preset
// mounting later would otherwise collide with whatever took it.
expect(() => registerEcho(ctx, RUN_CODE_NAME)).toThrow('is reserved')
})
it('reports the missing runtime against the agent\'s own mode', async () => {
const { ctx, systemPrompt } = await setup({ mode: 'native', runtime: false })
registerEcho(ctx)
const { scope, agent } = await mintAgentScope(ctx)
scope.ctx.tools.presentAs('both')
await expect(systemPrompt.assemble({ scope: agent }))
.rejects.toThrow('mode "both" requires a code runtime')
})
})

View File

@@ -1,7 +1,7 @@
/** Covers fail-closed per-call classification and model-schema isolation. */
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, {

View File

@@ -1,5 +1,5 @@
import { describe, expectTypeOf, it } from 'vitest'
import type { Context } from 'cordis'
import type { Context } from '@deepseek-ai/cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import type {

View File

@@ -23,7 +23,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
const catalog = await collectToolCatalog()
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
for (const entry of catalog) {
for (const schema of entry.schemas) {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'

View File

@@ -1,7 +1,7 @@
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { Events } from 'cordis'
import { createScope } from '@deepseek-ai/dsh-scope'
import { Context } from '@deepseek-ai/cordis'
import type { Events } from '@deepseek-ai/cordis'
import { bindScopeParent, createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
@@ -181,21 +181,84 @@ describe('restrict()', () => {
expect(ctx.tools.schemas(key).map(t => t.name)).toEqual(['b'])
})
it('fails loud on an unscoped call, an empty filter, and non-global names', async () => {
it('fails loud on an unscoped call, an empty filter, and names it does not inherit', async () => {
const ctx = await mount()
const { scope } = await mintAgentScope(ctx, 'a')
ctx.tools.register(tool('real'))
scope.ctx.tools.register(tool('local'))
expect(() => ctx.tools.restrict({ deny: ['real'] })).toThrow(/requires a scoped context/)
expect(() => scope.ctx.tools.restrict({})).toThrow(/no-op/)
expect(() => scope.ctx.tools.restrict({ allow: ['local'] })).toThrow(/unknown global tool "local"/)
expect(() => scope.ctx.tools.restrict({ allow: ['reall'] })).toThrow(/unknown global tool "reall"; known global tools: real/)
expect(() => scope.ctx.tools.restrict({ deny: ['ghost', 'wraith'] })).toThrow(/unknown global tools "ghost", "wraith"/)
// A scope's own registration is exempt from its own filter, so naming it
// is a caller error rather than a silent no-op.
expect(() => scope.ctx.tools.restrict({ allow: ['local'] })).toThrow(/unknown inherited tool "local"/)
expect(() => scope.ctx.tools.restrict({ allow: ['reall'] })).toThrow(/unknown inherited tool "reall".*Restrictable tools: real/s)
expect(() => scope.ctx.tools.restrict({ deny: ['ghost', 'wraith'] })).toThrow(/unknown inherited tools "ghost", "wraith"/)
const emptyCtx = await mount()
const { scope: emptyScope } = await mintAgentScope(emptyCtx, 'empty')
expect(() => emptyScope.ctx.tools.restrict({ deny: ['ghost'] }))
.toThrow(/known global tools: \(none\)/)
.toThrow(/Restrictable tools: \(none\)/)
})
})
describe('restrict() over an inherited scope layer', () => {
/** Mint a child scope parented to `parent`, as a subagent's creation window does. */
async function mintChild(ctx: Context, parentKey: Agent, name: string): Promise<{ scope: Scope; key: Agent }> {
const key = { id: name as SessionId } as Agent
bindScopeParent(key, parentKey)
let scope!: Scope
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, key) },
{ inject: ['tools', 'systemPrompt'] }))
return { scope, key }
}
it('filters tools the child inherits from an ancestor scope, not only global ones', async () => {
// The shape every preset deployment has: no model-facing row in the global
// layer, all of them contributed by an ancestor scope the child joined.
const ctx = await mount()
const parent = await mintAgentScope(ctx, 'parent')
parent.scope.ctx.tools.register(tool('bash'))
parent.scope.ctx.tools.register(tool('read'))
const child = await mintChild(ctx, parent.key, 'child')
expect(ctx.tools.schemas(child.key).map(t => t.name).sort()).toEqual(['bash', 'read'])
child.scope.ctx.tools.restrict({ deny: ['bash'] })
// Reading the exempt set as "the global layer" left this unfiltered, and
// the name unrestrictable in the first place.
expect(ctx.tools.schemas(child.key).map(t => t.name)).toEqual(['read'])
expect(await run(ctx, 'bash', child.key)).toBe('Error: unknown tool "bash"')
// The ancestor keeps its whole surface: a child's filter is its own.
expect(ctx.tools.schemas(parent.key).map(t => t.name).sort()).toEqual(['bash', 'read'])
})
it('keeps the child\'s own registrations outside its own filter', async () => {
// The delegation runtime registers a child's reporting and structured
// output tools into the child's own layer; an `allow` naming only the
// capabilities the child may use must not strip them.
const ctx = await mount()
const parent = await mintAgentScope(ctx, 'parent')
parent.scope.ctx.tools.register(tool('bash'))
parent.scope.ctx.tools.register(tool('read'))
const child = await mintChild(ctx, parent.key, 'child')
child.scope.ctx.tools.register(tool('report'))
child.scope.ctx.tools.restrict({ allow: ['read'] })
expect(ctx.tools.schemas(child.key).map(t => t.name).sort()).toEqual(['read', 'report'])
expect(await run(ctx, 'report', child.key)).toBe('ran:report')
})
it('lets an ancestor\'s restriction reach every scope nested inside it', async () => {
const ctx = await mount()
ctx.tools.register(tool('web'))
const parent = await mintAgentScope(ctx, 'parent')
parent.scope.ctx.tools.register(tool('bash'))
const child = await mintChild(ctx, parent.key, 'child')
parent.scope.ctx.tools.restrict({ deny: ['web'] })
expect(ctx.tools.schemas(child.key).map(t => t.name)).toEqual(['bash'])
expect(ctx.tools.schemas(parent.key).map(t => t.name)).toEqual(['bash'])
})
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from 'cordis'
import { Context } from '@deepseek-ai/cordis'
import { createUserMessage, CallId, HarnessError, type ContentBlock } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { Agent } from '@deepseek-ai/dsh-agent'