Merge branch 'codex/tool-json-schema-dsl' into codex/canonical-tool-output

# Conflicts:
#	docs/config-catalog.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/stdout.expected.jsonl
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-loop/tests/contract-regressions.spec.ts
#	packages/ui/acp/src/index.ts
#	packages/ui/tui/src/index.ts
This commit is contained in:
Tianyi Cui
2026-07-22 23:50:41 +08:00
159 changed files with 3918 additions and 362 deletions

View File

@@ -1,10 +1,11 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { makeBridgeHarness, textResponse, toolCallResponse, type BridgeHarness } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
import { encodeSessionReferenceUri, formatSessionReferenceMention } from '@deepseek-ai/dsh-session-reference'
/**
* End-to-end bridge specs over an in-memory transport: a real
@@ -329,6 +330,102 @@ describe('acp bridge', () => {
expect(JSON.stringify(user)).toContain('resource_link')
})
it('rejects canonical session references when the optional capability is not mounted', async () => {
harness = await makeBridgeHarness({ storageDir, script: [] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await expect(harness.client.prompt({
sessionId,
prompt: [{ type: 'resource_link', uri: encodeSessionReferenceUri(SessionId('source')), name: 'source' }],
})).rejects.toThrow(/session reference capability unavailable/)
expect(harness.ctx.agents.get(SessionId(sessionId))?.session.events).toHaveLength(0)
})
it('reports malformed inline session references at the ACP request boundary', async () => {
harness = await makeBridgeHarness({ storageDir, script: [] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await expect(harness.client.prompt({
sessionId,
prompt: [{ type: 'text', text: 'use dsh-session:IiJ' }],
})).rejects.toThrow(/invalid session reference/)
expect(harness.ctx.agents.get(SessionId(sessionId))?.session.events).toHaveLength(0)
})
it('prepares ACP session resource links and inline mentions before one atomic send', async () => {
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true, script: [textResponse('ok')] })
const source = harness.ctx.sessions.create(SessionId('source'), { meta: { cwd: '/source' } })
source.append('user/message', {
content: [{ type: 'text', text: 'source background' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const mention = formatSessionReferenceMention({ sessionId: source.id, label: 'source-inline' })
const result = await harness.client.prompt({
sessionId,
prompt: [
{ type: 'text', text: `use ${mention} and ` },
{ type: 'resource_link', uri: encodeSessionReferenceUri(source.id), name: 'source-link' },
],
})
expect(result.stopReason).toBe('end_turn')
const target = harness.ctx.agents.get(SessionId(sessionId))!.session
const user = target.events.find(event => event.type === 'user/message')
expect(user?.type === 'user/message' && user.data.envelope).toMatchObject({
displayContent: [{ type: 'text', text: 'use @source-inline and @source-link' }],
prefixContexts: [{
source: { kind: 'plugin', plugin: 'session-reference' },
meta: {
kind: 'session-reference',
references: [{ sessionId: 'source', label: 'source-inline' }],
},
}],
})
expect(target.events.some(event => event.type === 'context/message')).toBe(false)
const request = JSON.stringify(harness.adapter.requests[0]?.messages)
expect(request).toContain('untrusted, read-only snapshot')
expect(request).toContain('source background')
expect(request.indexOf('source background')).toBeLessThan(request.indexOf('## My request:'))
expect(request.indexOf('## My request:')).toBeLessThan(request.indexOf('use @source-inline and @source-link'))
})
it('rejects a failed referenced-session read before starting a turn', async () => {
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true, script: [] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await expect(harness.client.prompt({
sessionId,
prompt: [{ type: 'resource_link', uri: encodeSessionReferenceUri(SessionId('missing')), name: 'missing' }],
})).rejects.toThrow(/preparation failed/)
expect(harness.ctx.agents.get(SessionId(sessionId))?.session.events).toHaveLength(0)
})
it('cancels reference preparation before a turn is created', async () => {
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true, script: [] })
const source = harness.ctx.sessions.create(SessionId('source'))
const snapshot = await harness.ctx.sessionQuery.readSurface(source.id)
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
let releaseRead: (() => void) | undefined
const readSurface = vi.spyOn(harness.ctx.sessionQuery, 'readSurface').mockImplementationOnce(async () => {
await new Promise<void>((resolve) => { releaseRead = resolve })
return snapshot
})
const pending = harness.client.prompt({
sessionId,
prompt: [{ type: 'resource_link', uri: encodeSessionReferenceUri(source.id), name: 'source' }],
})
await vi.waitFor(() => { expect(releaseRead).toBeTypeOf('function') })
await harness.client.cancel({ sessionId })
await expect(pending).resolves.toEqual({ stopReason: 'cancelled' })
expect(harness.ctx.agents.get(SessionId(sessionId))?.session.events).toHaveLength(0)
releaseRead?.()
await Promise.resolve()
readSurface.mockRestore()
})
it('rejects a prompt for an unknown session', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })

View File

@@ -1,8 +1,11 @@
import { describe, expect, it } from 'vitest'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { TurnEndReason } from '@deepseek-ai/dsh-session'
import { SessionId } from '@deepseek-ai/dsh-session'
import { encodeSessionReferenceUri, formatSessionReferenceMention } from '@deepseek-ai/dsh-session-reference'
import type { ContentBlock as AcpContentBlock } from '@agentclientprotocol/sdk'
import {
acpPromptToReferencedPrompt,
acpPromptToText,
harnessBlockToAcpContent,
promptHasUnsupportedContent,
@@ -55,6 +58,35 @@ describe('acpPromptToText', () => {
})
})
describe('acpPromptToReferencedPrompt', () => {
it('extracts resource links and inline mentions while preserving ordinary links', () => {
const sessionId = SessionId('source/会话')
const prompt: AcpContentBlock[] = [
{ type: 'text', text: `compare ${formatSessionReferenceMention({ sessionId, label: 'inline' })} with ` },
{ type: 'resource_link', uri: encodeSessionReferenceUri(sessionId), name: 'linked' },
{ type: 'resource_link', uri: 'file:///x', name: 'x' },
]
expect(acpPromptToReferencedPrompt(prompt)).toEqual({
text: 'compare @inline with @linked\n[resource_link name="x" uri="file:///x"]\n',
references: [{ sessionId, label: 'inline' }, { sessionId, label: 'linked' }],
})
})
it('rejects malformed session resource links', () => {
expect(() => acpPromptToReferencedPrompt([
{ type: 'resource_link', uri: 'dsh-session:%%%', name: 'bad' },
])).toThrow(/invalid session reference URI/)
})
it('uses the decoded id for an empty resource name and ignores unsupported direct inputs', () => {
const sessionId = SessionId('source')
expect(acpPromptToReferencedPrompt([
{ type: 'resource_link', uri: encodeSessionReferenceUri(sessionId), name: '' },
{ type: 'image', mimeType: 'image/png', data: 'AA==' },
])).toEqual({ text: '@source', references: [{ sessionId, label: 'source' }] })
})
})
describe('promptHasUnsupportedContent', () => {
it('detects image, audio, and embedded resource blocks', () => {
expect(promptHasUnsupportedContent([{ type: 'image', mimeType: 'image/png', data: 'AA==' }])).toBe(true)

View File

@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { SessionId } from '@deepseek-ai/dsh-session'
import { encodeSessionReferenceUri } from '@deepseek-ai/dsh-session-reference'
import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
function commandUpdates(harness: BridgeHarness, sessionId: string) {
@@ -195,6 +196,28 @@ describe('ACP plugin commands', () => {
expect(harness.adapter.requests).toHaveLength(0)
})
it('keeps session-reference syntax opaque in direct command arguments', async () => {
harness = await makeBridgeHarness({ storageDir })
const command = vi.fn(() => ({ kind: 'success' as const }))
harness.ctx.commands.register({ name: 'direct', description: 'Direct', handler: command })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const sourceUri = encodeSessionReferenceUri(SessionId('source'))
await expect(harness.client.prompt({
sessionId,
prompt: [
{ type: 'text', text: `/direct valid=${sourceUri} malformed=dsh-session:IiJ` },
{ type: 'resource_link', name: 'source', uri: sourceUri },
],
})).resolves.toEqual({ stopReason: 'end_turn' })
expect(command).toHaveBeenCalledWith(expect.objectContaining({
rawInput: ` valid=${sourceUri} malformed=dsh-session:IiJ\n[resource_link name="source" uri=${JSON.stringify(sourceUri)}]\n`,
}))
expect(harness.adapter.requests).toHaveLength(0)
expect(harness.ctx.agents.get(SessionId(sessionId))?.session.events).toHaveLength(0)
})
it('maps session cancellation to the in-flight command signal and isolates other sessions', async () => {
harness = await makeBridgeHarness({ storageDir })
let started!: () => void

View File

@@ -31,6 +31,8 @@ import {
type Stream,
} from '@agentclientprotocol/sdk'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import SessionQueryService from '@deepseek-ai/dsh-session-query'
import SessionReferenceService from '@deepseek-ai/dsh-session-reference'
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as AcpPlugin from '../src/index.ts'
import { type AcpConfig } from '../src/index.ts'
@@ -192,6 +194,8 @@ export async function makeBridgeHarness(options: {
* tool + the bridge's own todo/write→plan mapping, not a stand-in.
*/
withTodo?: boolean
/** Mount exact session reads and cross-session snapshot preparation before ACP. */
withSessionReferences?: boolean
/** Plug the REAL `dsh-plan-mode` plugin so a test can drive the session-mode picker. */
withModes?: boolean
/**
@@ -217,6 +221,10 @@ export async function makeBridgeHarness(options: {
await ctx.plugin(CommandService)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root: options.storageDir })
await ctx.plugin(SessionQueryService)
if (options.withSessionReferences) {
await ctx.plugin(SessionReferenceService)
}
await ctx.plugin(UserInteractionService)
if (options.withAskUser) {
await ctx.plugin(ToolAskUser)

View File

@@ -0,0 +1,92 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { SessionId } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-session-title'
import { encodeSessionReferenceUri } from '@deepseek-ai/dsh-session-reference'
import { ACP_SESSION_REFERENCE_META_KEY } from '../src/index.ts'
import { makeBridgeHarness, type BridgeHarness } from './harness.ts'
describe('acp bridge — session/list', () => {
let storageDir: string
let harness: BridgeHarness | undefined
beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-list-')) })
afterEach(async () => {
await harness?.dispose()
harness = undefined
await rm(storageDir, { recursive: true, force: true })
})
it('advertises title-aware listing and reference metadata for loadable sessions', async () => {
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true })
const initialized = await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
expect(initialized.agentCapabilities?.sessionCapabilities?.list).toEqual({})
const cwd = process.cwd()
const { sessionId } = await harness.client.newSession({ cwd, mcpServers: [] })
const session = harness.ctx.agents.get(SessionId(sessionId))!.session
await harness.ctx.sessions.appendOutOfBand(session, 'session/title', {
title: 'Reference source title',
messageSeqs: [],
source: { kind: 'fallback' },
}, { kind: 'session-title' })
harness.ctx.sessions.create(SessionId('untitled'), { meta: { cwd: join(storageDir, 'other') } })
harness.ctx.sessions.create(SessionId('missing-cwd'))
const listed = await harness.client.listSessions({})
expect(listed.nextCursor).toBeUndefined()
expect(listed.sessions.map(item => item.sessionId)).toEqual(expect.arrayContaining([sessionId, 'untitled']))
expect(listed.sessions.map(item => item.sessionId)).not.toContain('missing-cwd')
const source = listed.sessions.find(item => item.sessionId === sessionId)
expect(source).toMatchObject({ cwd, title: 'Reference source title' })
expect(source?._meta?.[ACP_SESSION_REFERENCE_META_KEY]).toEqual({
uri: encodeSessionReferenceUri(SessionId(sessionId)),
})
expect(listed.sessions.find(item => item.sessionId === 'untitled')).not.toHaveProperty('title')
})
it('filters by normalized cwd and omits reference metadata without the optional capability', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const firstCwd = join(storageDir, 'first')
const secondCwd = join(storageDir, 'second')
const first = await harness.client.newSession({ cwd: firstCwd, mcpServers: [] })
await harness.client.newSession({ cwd: secondCwd, mcpServers: [] })
const listed = await harness.client.listSessions({ cursor: null, cwd: firstCwd })
expect(listed.sessions).toHaveLength(1)
expect(listed.sessions[0]).toMatchObject({ sessionId: first.sessionId, cwd: firstCwd })
expect(listed.sessions[0]?._meta).toBeUndefined()
await expect(harness.client.listSessions({ cwd: null })).resolves.toHaveProperty('sessions')
})
it('rejects unsupported cursors and relative cwd filters', async () => {
harness = await makeBridgeHarness({ storageDir })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
await expect(harness.client.listSessions({ cursor: 'next' })).rejects.toThrow('session/list does not paginate')
await expect(harness.client.listSessions({ cwd: 'relative' })).rejects.toThrow('session/list cwd must be absolute')
})
it('folds titles from persisted sessions in a fresh bridge', async () => {
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const cwd = process.cwd()
const { sessionId } = await harness.client.newSession({ cwd, mcpServers: [] })
const session = harness.ctx.agents.get(SessionId(sessionId))!.session
await harness.ctx.sessions.appendOutOfBand(session, 'session/title', {
title: 'Persisted reference title',
messageSeqs: [],
source: { kind: 'fallback' },
}, { kind: 'session-title' })
await harness.dispose()
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
await expect(harness.client.listSessions({ cwd })).resolves.toMatchObject({
sessions: [{ sessionId, cwd, title: 'Persisted reference title' }],
})
})
})

View File

@@ -209,6 +209,24 @@ describe('streamSessionEventUpdate', () => {
expect(updatesFor(evt('user/message', { content: [], source: { kind: 'user' } }))).toEqual([])
})
it('replays only the direct prompt from a prefixed user message', () => {
expect(updatesFor(evt('user/message', {
content: [
{ type: 'text', text: 'internal prefix' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'visible request' },
],
source: { kind: 'user' },
envelope: {
displayContent: [{ type: 'text', text: 'visible request' }],
prefixContexts: [{ source: { kind: 'plugin', plugin: 'reference' } }],
},
}))).toEqual([{
sessionUpdate: 'user_message_chunk',
content: { type: 'text', text: 'visible request' },
}])
})
it('can suppress user/message chunks for live prompt turns', () => {
expect(liveUpdatesFor(evt('user/message', {
content: [{ type: 'text', text: 'hi' }],