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

@@ -64,7 +64,7 @@ Providers stream token-sized deltas, so a raw log stores hundreds of `assistant/
`request/header` records a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, or `change`. `foldRequestHeader()` selects the latest snapshot; legacy delta events and the removed `fallback` reason are rejected. `messagePrefix` remains separate from derived history. See the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md).
`context/message` renders its `content` verbatim as a user-role message, and may attach JSON `meta` for replayable plugin state; metadata remains durable but is excluded from `deriveMessages()`.
`context/message` renders its `content` verbatim as a user-role message, and may attach JSON `meta` for replayable plugin state; metadata remains durable but is excluded from `deriveMessages()`. A `user/message` or `steering/message` with prompt-prefix context keeps the exact combined model bytes in `content` and stores a model-hidden `envelope` containing the direct `displayContent` and prefix context source/metadata descriptors. `displayPromptContent()` selects the human-facing prompt without changing derived history.
`tool/result` persists the model-facing content, optional internal failure identity, and optional presentation metadata. A tool's successful canonical `value` and human-readable canonical failure message remain execution-local; rendered error content is the replay-authoritative message. This preserves the existing event shape and does not change `SESSION_FORMAT_VERSION`.
@@ -99,7 +99,7 @@ Every `SessionEvent` carries two optional top-level fields (structural metadata)
#### What the model sees
The model receives projections of `user/message`, `assistant/message`, `tool/result`, `context/message`, and `steering/message` surface entries verbatim: each is a user- or assistant-role message carrying its content blocks unchanged. Tool calls live inside assistant messages. Chunks, boundaries, usage, hook records, todo records, and other log-only events add no message.
The model receives projections of `user/message`, `assistant/message`, `tool/result`, `context/message`, and `steering/message` surface entries verbatim: each is a user- or assistant-role message carrying its content blocks unchanged. A prompt envelope changes only human presentation; its prefix context and request delimiter are already present in the event content. Tool calls live inside assistant messages. Chunks, boundaries, usage, hook records, todo records, and other log-only events add no message.
#### Token effect

View File

@@ -11,9 +11,9 @@ import { isAbsolute } from 'node:path'
import { deepFreeze } from '@deepseek-ai/dsh-llm'
import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { Message } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, SessionId } from './types.ts'
import type { CreateSessionOptions, EpochHeader, OutOfBandSessionEventType, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType, TurnTrigger } from './types.ts'
import type { CreateSessionOptions, EpochHeader, OutOfBandSessionEventType, PromptMessageData, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType, TurnTrigger } from './types.ts'
import { snapshotJsonValue } from './json.ts'
import { SurfaceManager } from './surface.ts'
import type { SessionSurface } from './surface.ts'
@@ -29,6 +29,15 @@ export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from '
export { foldSurface, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts'
export { canonicalHeader, foldRequestHeader, headerEquals } from './request-header.ts'
/**
* Return the human-facing prompt blocks from a durable prompt message.
* @param data - ordinary or steering prompt event data.
* @returns the effective direct prompt, excluding baked prefix context.
*/
export function displayPromptContent(data: PromptMessageData): ContentBlock[] {
return data.envelope?.displayContent ?? data.content
}
/**
* Find the latest closed message-triggered turn, excluding injection and
* plugin-owned zero-step turns.
@@ -523,9 +532,11 @@ export class Session {
// trace/replay data.
switch (event.type) {
// Injected context and mid-turn steering project identically to a user
// prompt: content verbatim, in user role. context's `source`/`meta` and
// steering's `turn` are log-only and do not reach the model. Do NOT
// Injected context, ordinary prompts, and mid-turn steering project
// identically in user role: the event's model-facing content stays
// verbatim. A prompt envelope is model-hidden display metadata; its
// prefix bytes are already present in content. context's `source`/`meta`
// and steering's `turn` are also log-only. Do NOT
// re-add per-type framing (e.g. `<context>`/`<steering>`) here: framing is
// caller-owned — a producer bakes it into `content`, as workspace-context
// does with `<system-reminder>` — or, if reintroduced, must be driven by

View File

@@ -180,6 +180,37 @@ export interface EpochHeader {
*/
export type RequestHeaderReason = 'initial' | 'resume' | 'change'
/** Durable model-hidden annotation for one context baked into a prompt message. */
export interface PromptPrefixContext {
/** Producer provenance retained for transcript presentation and inspection. */
source: MessageSource
/** Opaque JSON state retained in the session event but hidden from the model. */
meta?: JsonValue
}
/**
* Human-facing view of a prompt whose exact model content includes prefixed
* context. `content` on the owning event remains the reconstructable model
* input; this envelope prevents transcript, title, and re-reference consumers
* from treating the baked context as direct human text.
*/
export interface PromptMessageEnvelope {
/** Effective user prompt after interception rewrites, without baked context. */
displayContent: ContentBlock[]
/** Ordered descriptors for contexts already baked into the event content. */
prefixContexts: PromptPrefixContext[]
}
/** Shared payload for ordinary and steering prompt messages. */
export interface PromptMessageData {
/** Exact model-facing blocks, including any baked prompt-prefix contexts. */
content: ContentBlock[]
/** Producer provenance for the direct prompt. */
source: MessageSource
/** Present only when prompt-prefix contexts were baked into `content`. */
envelope?: PromptMessageEnvelope
}
/**
* The merge-extensible, append-only source of truth for an agent interaction.
* Message history is derived from this log. Every event is lossless JSON and
@@ -206,7 +237,7 @@ export interface SessionEventMap {
/** Closes step `step` of turn `turn`. */
'step/end': { turn: number; step: number }
/** A user-visible prompt (the queued message claimed for this turn). */
'user/message': { content: ContentBlock[]; source: MessageSource }
'user/message': PromptMessageData
/**
* Durable record of a prompt veto and its reason. It is log-only: the blocked
* prompt never enters the model-visible surface, and its turn runs zero steps.
@@ -264,7 +295,7 @@ export interface SessionEventMap {
meta?: JsonValue
}
/** Steering content injected between steps of a running turn. */
'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }
'steering/message': PromptMessageData & { turn: number }
/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
'todo/write': { todos: TodoItem[] }
/**

View File

@@ -2,6 +2,7 @@ import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SessionStore, {
displayPromptContent,
findLastMessageTurnEnd,
SESSION_FORMAT_VERSION,
Session,
@@ -135,6 +136,35 @@ describe('Session', () => {
expect(steeringMessage!.content).toEqual([{ type: 'text', text: 'focus on tests' }])
})
it('derives baked prompt context while exposing only the direct prompt for display', () => {
const session = new Session(SessionId('prompt-envelope'))
const event = session.append('user/message', {
content: [
{ type: 'text', text: 'background' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'question' },
],
source: { kind: 'user' },
envelope: {
displayContent: [{ type: 'text', text: 'question' }],
prefixContexts: [{ source: { kind: 'plugin', plugin: 'reference' }, meta: { kind: 'card' } }],
},
}, { surfaceOp: 'append' })
expect(session.deriveMessages()).toEqual([{
role: 'user',
content: [
{ type: 'text', text: 'background' },
{ type: 'text', text: '\n\n## My request:\n' },
{ type: 'text', text: 'question' },
],
}])
expect(displayPromptContent(event.data)).toEqual([{ type: 'text', text: 'question' }])
expect(Object.isFrozen(event.data.envelope?.displayContent)).toBe(true)
expect(new Session(SessionId('prompt-envelope-replay'), session.events).deriveMessages())
.toEqual(session.deriveMessages())
})
it('keeps context meta durable in the event while hiding it from the projection', () => {
const session = new Session(SessionId('s2-raw'))
const meta = {