Merge branch 'codex/simp-agent-entry-state' into codex/simp-unify-agent-session-id

# Conflicts:
#	docs/architecture.md
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/coding-agent/tests/code-mode.e2e.ts
#	examples/coding-agent/tests/coding-task.e2e.ts
#	examples/coding-agent/tests/compaction.e2e.ts
#	examples/coding-agent/tests/full-loop.e2e.ts
#	examples/coding-agent/tests/todo-write.e2e.ts
#	examples/cordis-agent/tests/cordis-tools.e2e.ts
#	packages/bash/tool-bash/tests/integration.spec.ts
#	packages/bash/tool-bash/tests/tools.spec.ts
#	packages/compact/compact-basic/tests/compact-loop-repro.spec.ts
#	packages/context/time-context/tests/time-context.spec.ts
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/cordis/tool-cordis/tests/integration.spec.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/agent.ts
#	packages/core/agent-loop/src/index.ts
#	packages/core/agent-loop/tests/agent.spec.ts
#	packages/core/agent-loop/tests/cancel.spec.ts
#	packages/core/agent-loop/tests/config-session-id.spec.ts
#	packages/core/agent-loop/tests/contract-regressions.spec.ts
#	packages/core/agent-loop/tests/coverage-edges.spec.ts
#	packages/core/agent-loop/tests/interception.spec.ts
#	packages/core/agent-loop/tests/loop.spec.ts
#	packages/core/agent-loop/tests/properties.spec.ts
#	packages/core/agent-loop/tests/request-cache.e2e.ts
#	packages/core/agent-loop/tests/request-reconstruction.spec.ts
#	packages/core/agent-loop/tests/resume.spec.ts
#	packages/core/agent-loop/tests/scope-lifecycle.spec.ts
#	packages/core/agent-loop/tests/tool-order.spec.ts
#	packages/core/agent-loop/tests/turn-stop.spec.ts
#	packages/core/agent/src/types.ts
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/tests/agent-core.spec.ts
#	packages/examples/stdio-demo/README.md
#	packages/examples/stdio-demo/src/index.ts
#	packages/examples/stdio-demo/tests/stdio-agent.spec.ts
#	packages/fs/tool-fs/tests/fs-tools.e2e.ts
#	packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts
#	packages/hooks/hooks-claude/tests/bridge.spec.ts
#	packages/hooks/hooks-claude/tests/coverage.spec.ts
#	packages/hooks/hooks-codex/tests/bridge.spec.ts
#	packages/hooks/hooks-codex/tests/coverage.spec.ts
#	packages/subagent/subagent-fork/tests/multi-subagent.spec.ts
#	packages/subagent/subagent-fork/tests/subagent-fork.spec.ts
#	packages/subagent/subagent-inprocess/tests/structured.spec.ts
#	packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts
#	packages/subagent/subagent-spawn/tests/spawn.e2e.ts
#	packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts
#	packages/todo/tool-todo/tests/integration.spec.ts
#	packages/ui/acp/tests/dispose.spec.ts
#	packages/ui/acp/tests/edges.spec.ts
#	packages/workflow/workflow-workerthread/tests/integration.spec.ts
This commit is contained in:
Tianyi Cui
2026-07-18 12:21:15 +08:00
657 changed files with 36658 additions and 7514 deletions

View File

@@ -13,9 +13,9 @@ import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope'
import type { Scoped } from '@deepseek-ai/dsh-scope'
import type { ContentBlock, Message, MessageSource } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, SessionId } from './types.ts'
import type { CreateSessionOptions, EpochHeader, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType } from './types.ts'
import type { ContextEnvelope, CreateSessionOptions, EpochHeader, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType } from './types.ts'
import { snapshotJsonValue } from './json.ts'
import { SurfaceManager, isSurfaceEligibleType } from './surface.ts'
import { SurfaceManager } from './surface.ts'
import { foldRequestHeader } from './request-header.ts'
export * from './types.ts'
@@ -24,7 +24,6 @@ export type { JsonValue } from './json.ts'
export { interruptedTurnClosers } from './repair.ts'
export type { SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts'
export { foldSurface, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts'
export { isToolPairingBalanced } from './tool-pairing.ts'
export { canonicalHeader, foldRequestHeader, headerEquals } from './request-header.ts'
declare module 'cordis' {
@@ -131,43 +130,6 @@ function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHe
return deepFreeze(record as unknown as SessionHeader)
}
/** Validate the runtime shape of surface metadata after its JSON snapshot. */
function assertSurfaceMetadataShape(
type: string,
surfaceOp: unknown,
sourceEventSeqs: unknown,
): void {
const eligible = isSurfaceEligibleType(type)
if (!eligible) {
if (surfaceOp !== undefined || sourceEventSeqs !== undefined) {
throw new Error(`session event "${type}" is not surface-eligible and cannot carry surface metadata`)
}
return
}
if (surfaceOp === undefined) {
throw new Error(`session event "${type}" is surface-eligible and requires a surfaceOp marker`)
}
if (surfaceOp !== 'append') {
if (surfaceOp === null || typeof surfaceOp !== 'object' || Array.isArray(surfaceOp)) {
throw new Error(`session event "${type}" carries an invalid surfaceOp`)
}
const op = surfaceOp as Record<string, unknown>
const keys = Object.keys(op)
if (keys.length !== 3 || !Object.hasOwn(op, 'op') || !Object.hasOwn(op, 'start') || !Object.hasOwn(op, 'end')
|| op['op'] !== 'replace'
|| typeof op['start'] !== 'number' || !Number.isSafeInteger(op['start']) || op['start'] < 0
|| typeof op['end'] !== 'number' || !Number.isSafeInteger(op['end']) || op['end'] < 0) {
throw new Error(`session event "${type}" carries an invalid replace surfaceOp`)
}
}
if (sourceEventSeqs !== undefined) {
if (!Array.isArray(sourceEventSeqs)
|| sourceEventSeqs.some(seq => typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0)) {
throw new Error(`session event "${type}" sourceEventSeqs must contain non-negative safe integers`)
}
}
}
/** Validate the fixed event envelope after one-pass JSON materialization. */
function assertSessionEventEnvelope(value: Record<string, unknown>, index: number): asserts value is SessionEvent {
const event = value
@@ -184,6 +146,30 @@ function assertSessionEventEnvelope(value: Record<string, unknown>, index: numbe
|| !Object.hasOwn(event, 'data')) {
throw new Error(`seed event at index ${index} has an invalid event envelope`)
}
assertCurrentLlmShape(event, index)
}
/** Reject pre-provider request headers and assistant messages at the seed/load boundary. */
function assertCurrentLlmShape(event: Record<string, unknown>, index: number): void {
const data = event['data']
if (typeof data !== 'object' || data === null) return
const record = data as Record<string, unknown>
if (event['type'] === 'request/header') {
const header = record['header']
const config = typeof header === 'object' && header !== null ? (header as Record<string, unknown>)['config'] : undefined
if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`)
}
if (event['type'] === 'assistant/message' && !hasProviderModel(record['provenance'])) {
throw new Error(`seed assistant/message at index ${index} lacks provider/model provenance`)
}
}
/** Whether an unknown value carries the current provider/model pair. */
function hasProviderModel(value: unknown): boolean {
if (typeof value !== 'object' || value === null) return false
const pair = value as Record<string, unknown>
return typeof pair['provider'] === 'string' && pair['provider'].length > 0
&& typeof pair['model'] === 'string' && pair['model'].length > 0
}
/** Reject request-header vocabulary removed with the legacy delta codec. */
@@ -241,6 +227,22 @@ interface SessionEntry {
/** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
const attachments = new WeakMap<Session, SessionEntry>()
/**
* Render one context contribution exactly as it will appear in model history.
* @param content - content blocks supplied by the context producer.
* @param source - attribution used by the canonical context envelope.
* @param envelope - canonical tagged framing or caller-owned raw framing.
* @returns a detached block list ready for the derived model transcript.
*/
export function renderContextContent(
content: ContentBlock[],
source: MessageSource,
envelope: ContextEnvelope = 'context',
): ContentBlock[] {
const cloned = structuredClone(content)
return envelope === 'raw' ? cloned : renderTagged('context', cloned, source)
}
/**
* An event-sourced session: an append-only log of {@link SessionEvent}s.
*
@@ -249,13 +251,15 @@ const attachments = new WeakMap<Session, SessionEntry>()
*/
export class Session {
private log: SessionEvent[] = []
/** Incremental acceptance state, kept separate from the public lazy view. */
private readonly surfaceValidator = new SurfaceManager(this.log)
/**
* Derived surface — a cached order of message-producing event sequences.
* Lazily rebuilt from `surfaceOp` markers in the log; processes only new
* events (delta) on each access — the log is append-only, so prior events
* never change.
* `append`. Undefined until first accessed (including after fork/seed).
* Undefined until first accessed (including after fork/seed).
*/
private _surface: SurfaceManager | undefined
@@ -289,7 +293,7 @@ export class Session {
// `seq = log.length` contract the whole system relies on). Without this,
// a bad seed would surface only later as a backend rejection or a silent
// divergence between the live log and disk.
this.log = Array.from(seed, (source, index) => {
for (const [index, source] of seed.entries()) {
// The seed is a persistence/replay boundary: validate and detach the
// complete event in one lossless-JSON pass.
const snapshot = snapshotJsonValue(source)
@@ -301,20 +305,16 @@ export class Session {
if (snapshot.seq !== index) {
throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`)
}
// Surface-eligible events MUST carry a surfaceOp marker — the surface is
// the sole source of derived history, so a marker-less message event
// would load fine yet vanish from deriveMessages(). `append` enforces
// this at compile time via its typed overload; a seed arrives as raw
// SessionEvent[] (replay/fork/load), bypassing that, so re-check at
// runtime here rather than silently resuming with empty history.
const structural = snapshot as SessionEvent & { surfaceOp?: unknown; sourceEventSeqs?: unknown }
// A seed is accepted incrementally through the same transition as a
// live append and a full-log fold. The candidate is planned before it
// enters `log`, so a failure cannot partially mutate the surface.
try {
assertSurfaceMetadataShape(snapshot.type, structural.surfaceOp, structural.sourceEventSeqs)
this.surfaceValidator.validateNext(snapshot)
} catch (error: unknown) {
throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : 'invalid surface metadata'}`)
}
return deepFreeze(snapshot)
})
this.log.push(deepFreeze(snapshot))
}
}
this.header = snapshotSessionHeader(id, header)
}
@@ -361,7 +361,10 @@ export class Session {
* @throws if `data` or surface metadata is not losslessly JSON-serializable
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
* circular reference, sparse array, or an exotic object such as
* Map/Set/Date/class instance). One recursive pass reads, validates, and
* Map/Set/Date/class instance), or when the candidate violates the
* canonical surface contract (marker shape and eligibility, unique
* earlier provenance, positional replacement validity, and complete
* shadowed-node coverage). One recursive pass reads, validates, and
* copies each nested value once, so a stateful getter cannot supply one value
* to validation and another to storage. The event log is the durable source
* of truth, so a bad event fails at the append site rather than later during
@@ -388,25 +391,21 @@ export class Session {
if (surfaceMetadataSnapshot === undefined) {
throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`)
}
assertSurfaceMetadataShape(
type,
(surfaceMetadataSnapshot as { surfaceOp?: unknown }).surfaceOp,
(surfaceMetadataSnapshot as { sourceEventSeqs?: unknown }).sourceEventSeqs,
)
const entry = attachments.get(this)
if (entry?.appending) {
throw new Error('session append cannot reenter while another append is being published')
}
const event = deepFreeze({
type,
seq: this.log.length,
time: Date.now(),
data: dataSnapshot,
...(surfaceMetadataSnapshot as { surfaceOp?: unknown; sourceEventSeqs?: unknown }),
} as unknown as SessionEvent<T>)
this.surfaceValidator.validateNext(event as SessionEvent)
if (entry !== undefined) entry.appending = true
try {
const event = deepFreeze({
type,
seq: this.log.length,
time: Date.now(),
data: dataSnapshot,
...surfaceMetadataSnapshot,
} as unknown as SessionEvent<T>)
let callbacks: SessionCallback[] | undefined
const callbackArgs: unknown[] = [this, event]
if (entry !== undefined) {
@@ -526,7 +525,7 @@ export class Session {
// max-tokens step's usage and must not inject a content-less assistant
// turn into the provider transcript.
if (event.data.content.length === 0) return null
return { role: 'assistant', content: event.data.content }
return { role: 'assistant', content: event.data.content, provenance: event.data.provenance }
}
case 'tool/result': {
const { callId, content, isError } = event.data
@@ -536,8 +535,8 @@ export class Session {
}
}
case 'context/message': {
const { content, source } = event.data
return { role: 'user', content: renderTagged('context', content, source) }
const { content, source, envelope } = event.data
return { role: 'user', content: renderContextContent(content, source, envelope) }
}
case 'steering/message': {
const { content, source } = event.data

View File

@@ -61,40 +61,106 @@ interface SurfaceFoldState {
replaceGeneration: number
}
/** Create one empty fold state. */
/** A validated replacement transition that has not mutated fold state yet. */
interface SurfaceReplacePlan extends SurfaceFoldReplacement {
kind: 'replace'
startIdx: number
endIdx: number
}
/** One validated surface transition that has not mutated fold state yet. */
type SurfacePlan =
| { kind: 'append'; seq: number }
| SurfaceReplacePlan
/** Create an empty surface fold state. */
function createFoldState(): SurfaceFoldState {
return { nodes: [], replaceGeneration: 0 }
}
/** Apply one event and return replacement metadata when one occurred. */
function applySurfaceEvent(
state: SurfaceFoldState,
event: SessionEvent,
): SurfaceFoldReplacement | undefined {
if (!isSurfaceEligibleType(event.type)) return
if (!isSurfaceEvent(event)) {
throw new Error(`surface event "${event.type}" (seq ${event.seq}) carries no surfaceOp marker`)
}
if (event.surfaceOp === 'append') {
state.nodes.push(event.seq)
/** Whether a runtime value is a non-negative safe event sequence. */
function isEventSeq(value: unknown): value is number {
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
}
/** Whether a runtime value is the exact positional-replacement shape. */
function isReplaceOp(value: object): value is Extract<SurfaceOp, { op: 'replace' }> {
const op = value as Record<string, unknown>
return Object.keys(op).length === 3
&& Object.hasOwn(op, 'op')
&& Object.hasOwn(op, 'start')
&& Object.hasOwn(op, 'end')
&& op['op'] === 'replace'
&& isEventSeq(op['start'])
&& isEventSeq(op['end'])
}
/** Validate event-local surface eligibility and return its operation. */
function surfaceOpOf(event: SessionEvent): SurfaceOp | undefined {
const raw = event as SessionEvent & { surfaceOp?: unknown; sourceEventSeqs?: unknown }
if (!isSurfaceEligibleType(event.type)) {
if (raw.surfaceOp !== undefined) {
throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`)
}
if (raw.sourceEventSeqs !== undefined) {
throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`)
}
return
}
const op = raw.surfaceOp
if (op === undefined) {
throw new Error(`session event "${event.type}" is surface-eligible and requires a surfaceOp marker`)
}
if (op === 'append') return op
if (op === null || typeof op !== 'object' || Array.isArray(op)) {
throw new Error(`session event "${event.type}" carries an invalid surfaceOp`)
}
if (!isReplaceOp(op)) {
throw new Error(`session event "${event.type}" carries an invalid replace surfaceOp`)
}
return op
}
const shadowedSeqs = replaceSurface(state, event.seq, event.surfaceOp)
return {
seq: event.seq,
start: event.surfaceOp.start,
end: event.surfaceOp.end,
shadowedSeqs,
/** Validate provenance against prior log entries and the replacement range. */
function assertProvenance(
event: SessionEvent,
shadowedSeqs: readonly number[],
): void {
const raw = (event as SessionEvent & { sourceEventSeqs?: unknown }).sourceEventSeqs
const sources = new Set<number>()
if (raw !== undefined) {
if (!Array.isArray(raw)) {
throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`)
}
if (raw.length === 0 && event.type !== 'assistant/message') {
throw new Error('sourceEventSeqs must not be empty except on assistant/message')
}
let nonEarlierSource: number | undefined
for (const source of raw) {
if (!isEventSeq(source)) {
throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`)
}
sources.add(source)
if (nonEarlierSource === undefined && source >= event.seq) nonEarlierSource = source
}
if (sources.size !== raw.length) {
throw new Error('sourceEventSeqs must not contain duplicates')
}
if (nonEarlierSource !== undefined) {
throw new Error(`sourceEventSeqs must reference earlier events: ${nonEarlierSource} >= current seq ${event.seq}`)
}
}
const missing = shadowedSeqs.filter(seq => !sources.has(seq))
if (missing.length > 0) {
throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(', ')}`)
}
}
/** Replace one inclusive surface range and return the removed sequences. */
function replaceSurface(
/** Locate one replacement range without mutating the current fold state. */
function replacementRange(
state: SurfaceFoldState,
newSeq: number,
op: Extract<SurfaceOp, { op: 'replace' }>,
): number[] {
): Pick<SurfaceReplacePlan, 'startIdx' | 'endIdx' | 'shadowedSeqs'> {
const startIdx = state.nodes.indexOf(op.start)
if (startIdx === -1) {
throw new Error(`surface replace: start seq ${op.start} not found in surface`)
@@ -106,29 +172,78 @@ function replaceSurface(
if (startIdx > endIdx) {
throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`)
}
return {
startIdx,
endIdx,
shadowedSeqs: state.nodes.slice(startIdx, endIdx + 1),
}
}
const shadowedSeqs = state.nodes.splice(startIdx, endIdx - startIdx + 1, newSeq)
state.replaceGeneration += 1
return shadowedSeqs
/** Validate one event at its replay boundary and prepare its atomic fold transition. */
function planSurfaceEvent(
state: SurfaceFoldState,
event: SessionEvent,
expectedSeq: number,
): SurfacePlan | undefined {
if (event.seq !== expectedSeq) {
throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`)
}
const surfaceOp = surfaceOpOf(event)
if (surfaceOp === undefined) return
if (surfaceOp === 'append') {
assertProvenance(event, [])
return { kind: 'append', seq: event.seq }
}
const range = replacementRange(state, surfaceOp)
assertProvenance(event, range.shadowedSeqs)
return {
kind: 'replace',
seq: event.seq,
start: surfaceOp.start,
end: surfaceOp.end,
...range,
}
}
/** Apply one event and return replacement metadata only when one occurred. */
function applySurfaceEvent(
state: SurfaceFoldState,
event: SessionEvent,
expectedSeq: number,
): SurfaceFoldReplacement | undefined {
const plan = planSurfaceEvent(state, event, expectedSeq)
if (plan?.kind === 'append') {
state.nodes.push(plan.seq)
} else if (plan?.kind === 'replace') {
state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq)
state.replaceGeneration += 1
}
if (plan?.kind !== 'replace') return
return {
seq: plan.seq,
start: plan.start,
end: plan.end,
shadowedSeqs: plan.shadowedSeqs,
}
}
/**
* Replay a complete event log through the canonical surface fold.
* @param events - events in contiguous seq order.
* Replay a complete session log through the canonical surface fold.
* @param events - session events in contiguous seq order.
* @returns detached current sequences and replacement history.
* @throws when a surface marker is missing or names an invalid range.
* @throws when an event violates surface metadata, provenance, or range rules.
*/
export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult {
const state = createFoldState()
const replacements: SurfaceFoldReplacement[] = []
for (const event of events) {
const replacement = applySurfaceEvent(state, event)
for (const [index, event] of events.entries()) {
const replacement = applySurfaceEvent(state, event, index)
if (replacement !== undefined) replacements.push(replacement)
}
return { nodes: [...state.nodes], replacements }
}
/** Incremental ordered surface view over an append-only session log. */
/** Incremental ordered surface view and append-boundary validator. */
export class SurfaceManager {
/** Shared transition state; replacement history is not retained. */
private _state = createFoldState()
@@ -137,6 +252,15 @@ export class SurfaceManager {
constructor(private log: readonly SessionEvent[]) {}
/**
* Validate the next candidate without mutating the committed surface.
* @param event - candidate event that has not entered the log yet.
*/
validateNext(event: SessionEvent): void {
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
planSurfaceEvent(this._state, event, this.log.length)
}
/** Monotonic count of folded positional replacements. */
get replaceGeneration(): number {
if (this._lastProcessedSeq < this.log.length - 1) this._processDelta()
@@ -153,8 +277,8 @@ export class SurfaceManager {
private _processDelta(): void {
for (let i = this._lastProcessedSeq + 1; i < this.log.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- bounded by the loop condition
applySurfaceEvent(this._state, this.log[i]!)
applySurfaceEvent(this._state, this.log[i]!, i)
this._lastProcessedSeq = i
}
this._lastProcessedSeq = this.log.length - 1
}
}

View File

@@ -1,56 +0,0 @@
/**
* Tool-pairing balance over a session surface. Compaction changes surface
* positions, so safe cuts are derived from tool-call/result content on the
* surface rather than step markers in the append-only log.
* @module @deepseek-ai/dsh-session/tool-pairing
*/
import type { SessionEvent } from './types.ts'
/**
* The tool-pairing delta of a surface node: how it shifts the count of
* unanswered tool calls. An `assistant/message` opens one bracket per
* `tool-call` block; a `tool/result` closes one; every other surface node
* (`user/message`, `context/message`, `steering/message`, a usage-only
* `assistant/message` with no tool-call blocks) is pairing-neutral.
*/
function nodeDelta(event: SessionEvent): number {
switch (event.type) {
case 'assistant/message':
return event.data.content.filter(block => block.type === 'tool-call').length
case 'tool/result':
return -1
// Non-pairing surface nodes and every non-surface event contribute nothing.
default:
return 0
}
}
/**
* Check that a surface cut does not split a tool call from its result. A region
* is safe to collapse only when both edge cuts return true.
*
* @param nodes - surface event sequence numbers in head-to-tail order.
* @param events - the session log indexed by those sequence numbers.
* @param beforeSeq - event immediately after the cut; null or an absent seq means after-tail.
* @returns whether every call before the cut is answered before it.
* @throws if a result appears without a preceding open call.
*/
export function isToolPairingBalanced(
nodes: readonly number[],
events: readonly SessionEvent[],
beforeSeq: number | null,
): boolean {
let depth = 0
for (const seq of nodes) {
if (seq === beforeSeq) return depth === 0
// seq is a surface event sequence, always a valid log index by construction.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
depth += nodeDelta(events[seq]!)
if (depth < 0) {
throw new Error(`tool-pairing balance: tool/result at surface seq ${seq} has no matching tool-call (corrupt surface)`)
}
}
// A missing cut sequence means the after-tail boundary.
return depth === 0
}

View File

@@ -1,5 +1,9 @@
import type { Branded } from '@deepseek-ai/dsh-brand'
import type { CallId, ContentBlock, LlmCallConfig, Message, MessageSource, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm'
import type { AssistantProvenance, CallId, ContentBlock, LlmCallConfig, Message, MessageSource, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm'
import type { JsonValue } from './json.ts'
/** Canonical context-tag framing, or caller-owned framing rendered verbatim. */
export type ContextEnvelope = 'context' | 'raw'
/** Identifies one session in the store (and its persistence artifacts). */
export type SessionId = Branded<'SessionId'>
@@ -143,7 +147,7 @@ export interface TodoItem {
* canonical empty optional fields are absent.
*/
export interface EpochHeader {
/** The conversation's call configuration (model + sampling scalars). */
/** The conversation's call configuration (provider, model, and sampling scalars). */
config: LlmCallConfig
/** Rendered system prompt text; absent for a system-less request. */
system?: string
@@ -201,9 +205,16 @@ export interface SessionEventMap {
/**
* In-session context injection (file-change notices, subdir AGENTS.md,
* skill content, cron notifications, …). Rendered into the derived history
* as tagged synthetic context — NOT a user prompt.
* as synthetic context — NOT a user prompt. `envelope: 'raw'` lets a caller
* own the complete model-facing frame; `meta` is durable JSON state omitted
* from the model projection.
*/
'context/message': { content: ContentBlock[]; source: MessageSource }
'context/message': {
content: ContentBlock[]
source: MessageSource
envelope?: ContextEnvelope
meta?: JsonValue
}
/** Raw stream chunk — token-level replay fidelity. */
'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
/**
@@ -212,7 +223,7 @@ export interface SessionEventMap {
* the model output and its accounting travel together (there is no separate
* usage record). `usage` is absent when the adapter reported none.
*/
'assistant/message': { turn: number; step: number; content: ContentBlock[]; usage?: TokenUsage }
'assistant/message': { turn: number; step: number; content: ContentBlock[]; provenance: AssistantProvenance; usage?: TokenUsage }
/**
* The model requested one tool invocation: `name` with the raw `arguments`
* JSON string exactly as the model produced it (unparsed). `callId` pairs the
@@ -288,6 +299,12 @@ export type SurfaceOp =
*/
export interface SurfaceIntent {
surfaceOp: SurfaceOp
/**
* Complete known provenance source set. `assistant/message` may use a
* present empty array for a known empty provider stream; omission means its
* provenance was not recorded. Other surface events require a non-empty set
* when this field is present.
*/
sourceEventSeqs?: number[]
}
@@ -316,7 +333,9 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
/**
* Seq numbers of events that are provenance sources of this event
* (e.g. the `assistant/chunk` seqs that built an `assistant/message`,
* or the surface nodes shadowed by a compaction replace node).
* or the surface nodes shadowed by a compaction replace node). An
* `assistant/message` may carry a present empty array for a known empty
* provider stream; omission means unrecorded provenance.
*/
sourceEventSeqs?: number[]
/** How this event entered the surface; absent for non-surface events. */