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

@@ -117,6 +117,55 @@ describe('agent/prompt-submit', () => {
expect(sent).toContain('extra ctx')
})
it('bakes prompt-prefix contexts and a request delimiter into one durable user message', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('prefixed'), { provider: 'mock', model: 'mock' })
ctx.on('agent/prompt-submit', async (_agent, _content, _source, _signal, next): Promise<PromptDecision> => {
const downstream = await next()
return downstream.kind === 'block'
? downstream
: { ...downstream, content: [{ type: 'text', text: 'rewritten request' }] }
})
agent.send([{ type: 'text', text: 'original request' }], {
contexts: [{
content: [{ type: 'text', text: 'untrusted prefix' }],
source: { kind: 'plugin', plugin: 'prefix' },
placement: 'prompt-prefix',
meta: { kind: 'prefix-card' },
}],
})
await waitForIdle(ctx, agent)
const log = events(agent)
const user = log.find(event => event.type === 'user/message')
expect(user?.type === 'user/message' && user.data).toEqual({
content: [
{ type: 'text', text: 'untrusted prefix' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'rewritten request' },
],
source: { kind: 'user' },
envelope: {
displayContent: [{ type: 'text', text: 'rewritten request' }],
prefixContexts: [{
source: { kind: 'plugin', plugin: 'prefix' },
meta: { kind: 'prefix-card' },
}],
},
})
expect(log.some(event => event.type === 'context/message')).toBe(false)
expect(adapter.requests[0]?.messages.at(-1)).toEqual({
role: 'user',
content: [
{ type: 'text', text: 'untrusted prefix' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'rewritten request' },
],
})
})
it('runs pre-step after prompt rewrites and injected context become durable', async () => {
const adapter = new MockAdapter([textResponse('ok')])
const ctx = await harness(adapter)
@@ -154,7 +203,9 @@ describe('agent/prompt-submit', () => {
const reasons: TurnEndReason[] = []
ctx.on('session/event', (_s, event: SessionEvent) => { if (event.type === 'turn/end') reasons.push(event.data.reason) })
send(agent, 'do something')
agent.send([{ type: 'text', text: 'do something' }], {
contexts: [{ content: [{ type: 'text', text: 'must be dropped' }], source: { kind: 'plugin', plugin: 'test' } }],
})
await waitForIdle(ctx, agent)
// the model was never called
@@ -164,6 +215,7 @@ describe('agent/prompt-submit', () => {
expect(log.some(e => e.type === 'turn/start')).toBe(true)
expect(log.some(e => e.type === 'turn/end')).toBe(true)
expect(log.some(e => e.type === 'user/message')).toBe(false)
expect(log.some(e => e.type === 'context/message')).toBe(false)
expect(log.some(e => e.type === 'step/start')).toBe(false)
// the veto is recorded durably as a prompt/blocked in the open turn
const blocked = log.find(e => e.type === 'prompt/blocked')