diff --git a/docs/core-data-structures/compaction.i18n.yaml b/docs/core-data-structures/compaction.i18n.yaml index 09dedca673..89783b5d16 100644 --- a/docs/core-data-structures/compaction.i18n.yaml +++ b/docs/core-data-structures/compaction.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/core-data-structures/compaction.md -compaction.md: d3db21fb84cb82a4ba3bb8c16731b0e9f04c160a -compaction.zh.md: e96fff729b7a2d6375e6b471ee343a59f0f75a07 +compaction.md: 911b71d00fa4b42e9cdfa67f67d4e9b29e354a4a +compaction.zh.md: 643a116ff2edbbb53d300b4f5ff0ad36d401130b diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index d3db21fb84..911b71d00f 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -22,7 +22,7 @@ These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` b ## `CompactionResult` -What a successful compaction returns to its caller: the bookkeeping-event seqs, safe summary projection, optional complete provider output, shadowed range and seqs, and estimated token count. +What a successful compaction returns to its caller: the bookkeeping-event seqs, safe summary projection, shadowed range and seqs, and estimated token count. ```ts type-equiv /** Result of a successful compaction operation. */ @@ -35,8 +35,6 @@ interface CompactionResult { endSeq: number /** The summary content blocks produced by the backend. */ summary: ContentBlock[] - /** Complete provider output before the backend's safe summary projection. */ - rawOutput?: ContentBlock[] /** * The surface-boundary pair that was shadowed: the seqs of the first * (`start`) and last (`end`) surface nodes of the replaced range. A diff --git a/docs/core-data-structures/compaction.zh.md b/docs/core-data-structures/compaction.zh.md index e96fff729b..643a116ff2 100644 --- a/docs/core-data-structures/compaction.zh.md +++ b/docs/core-data-structures/compaction.zh.md @@ -22,7 +22,7 @@ ## `CompactionResult` -成功压缩向调用方返回:记账事件 seq、安全摘要投影、可选的完整 provider 输出、被遮蔽的范围与 seq,以及估算 token 数。 +成功压缩向调用方返回:记账事件 seq、安全摘要投影、被遮蔽的范围与 seq,以及估算 token 数。 ```ts type-equiv /** Result of a successful compaction operation. */ @@ -35,8 +35,6 @@ interface CompactionResult { endSeq: number /** The summary content blocks produced by the backend. */ summary: ContentBlock[] - /** Complete provider output before the backend's safe summary projection. */ - rawOutput?: ContentBlock[] /** * The surface-boundary pair that was shadowed: the seqs of the first * (`start`) and last (`end`) surface nodes of the replaced range. A diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index 63de9ccf2d..92577e77de 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -28,13 +28,16 @@ export type { EngineStoreHandle, EngineStoreInstance, ObservableSnapshot, SnapshotStore, } from './contract/store.ts' export type { - AssistantBlock, AssistantMessageNode, AssistantProvenanceView, AssistantRequestConfig, - AssistantTiming, CodeSubCall, ComposerPhase, ContextMessageNode, - CompactionRequestView, ConversationContext, ConversationContextOriginKind, - ConversationNode, ConversationPromptChange, ConversationPromptSnapshot, ModelRequestView, + AssistantBlock, AssistantMessageNode, AssistantTiming, CodeSubCall, ComposerPhase, ContextMessageNode, + ConversationNode, ConversationSnapshot, QueuedMessage, RunningToolCall, SteeringMessageNode, TodoItem, ToolResultNode, UnknownSurfaceNode, UserMessageNode, } from './sessions/conversation.ts' +export type { + AssistantProvenanceView, AssistantRequestConfig, CompactionRequestView, + ConversationContext, ConversationContextOriginKind, ConversationPromptChange, + ConversationPromptSnapshot, ModelRequestView, +} from './sessions/inspection.ts' export { PendingWait } from './sessions/pending.ts' export type { PendingInteraction, PendingKind, PendingPayloads } from './sessions/pending.ts' export type { SessionId } from '@deepseek-ai/dsh-client-connection/client' diff --git a/packages/client/runtime/src/client/sessions/conversation.ts b/packages/client/runtime/src/client/sessions/conversation.ts index 91d1a03859..234a25c3b5 100644 --- a/packages/client/runtime/src/client/sessions/conversation.ts +++ b/packages/client/runtime/src/client/sessions/conversation.ts @@ -3,12 +3,15 @@ // substructures keep their references (the React.memo premise). callId/approvalId stay plain // string here (narrow to real brands when convenient). -import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm/types' +import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' import type { TodoItem } from '@deepseek-ai/dsh-session/types' import type { RpcError, SessionId, ToolCallView, ToolResultView, } from '@deepseek-ai/dsh-client-connection/client' import type { PendingInteraction } from './pending.ts' +import type { + AssistantProvenanceView, AssistantRequestConfig, ConversationContext, SessionInspectionSnapshot, +} from './inspection.ts' export type { TodoItem } @@ -51,7 +54,6 @@ export interface UserMessageNode { time: number content: readonly ContentBlock[] source: unknown - meta?: unknown } /** Recorded boundaries used to derive assistant latency and throughput. */ @@ -64,24 +66,6 @@ export interface AssistantTiming { completedTime: number } -/** Request configuration recorded in the effective header for one assistant response. */ -export interface AssistantRequestConfig { - provider: string - model: string - purpose?: string - thinking?: string - reasoningEffort?: string - temperature?: number - maxTokens?: number - stop?: readonly string[] -} - -/** Stable provider/model identity attached to one assistant response. */ -export interface AssistantProvenanceView { - provider: string - model: string -} - /** A finalized (or interruption-frozen) assistant message. */ export interface AssistantMessageNode { kind: 'assistant' @@ -110,7 +94,6 @@ export interface SteeringMessageNode { turn: number content: readonly ContentBlock[] source: unknown - meta?: unknown } /** A context/system injection surfaced in the flow. */ @@ -230,91 +213,6 @@ export type OpenState = 'cold' | 'loading' | 'open' | 'error' */ export type ComposerPhase = 'blank' | 'engaging' | 'active' -/** Operation that started a new append-only model context. */ -export type ConversationContextOriginKind = 'compaction' | 'rewind' | 'rewrite' - -/** Latest complete model request header in force within one context generation. */ -export interface ConversationPromptSnapshot { - /** Provider/model and sampling configuration from the latest effective request header. */ - config?: AssistantRequestConfig - /** Rendered system prompt text; empty when the request had no system prompt. */ - system: string - /** Complete tool catalog sent with the request, including tools that were never called. */ - tools: readonly ToolSchema[] -} - -/** One system-prompt/tool-catalog state that became effective in the request timeline. */ -export interface ConversationPromptChange { - /** Sequence of the request/header event that introduced this state. */ - seq: number - /** Unix epoch ms from the request/header event. */ - time: number - /** How the model-visible system configuration differs from the prior recorded state. */ - kind: 'initial' | 'system' | 'tools' | 'system-and-tools' - /** Complete state effective from this event onward. */ - prompt: ConversationPromptSnapshot - /** State immediately before this change; absent for the initial header. */ - previous?: ConversationPromptSnapshot -} - -/** One immutable model-context generation reconstructed from surface replacements. */ -export interface ConversationContext { - /** Zero-based generation within the session; stable across later appends. */ - id: number - /** Previous generation in this session; absent for the initial context. */ - parentId?: number - /** Why this generation exists; absent for the initial context. */ - origin?: ConversationContextOriginKind - /** Event seq of the replacement that created this generation. */ - originSeq?: number - /** Unix epoch ms of the replacement that created this generation. */ - createdAt?: number - /** Latest request header observed in this generation, inherited until a later header replaces it. */ - prompt?: ConversationPromptSnapshot - /** Final frozen nodes for historical generations, or current folded nodes for the tail. */ - nodes: readonly ConversationNode[] -} - -/** One auxiliary compaction model request reconstructed from its durable lifecycle events. */ -export interface CompactionRequestView { - startSeq: number - turn: number - startedAt: number - completedAt: number | null - status: 'running' | 'complete' | 'error' - error?: string - summarySeq?: number - replacementSeq?: number - summary?: readonly ContentBlock[] - rawOutput?: readonly ContentBlock[] - provenance?: AssistantProvenanceView - requestConfig?: AssistantRequestConfig - usage?: unknown -} - -/** One ordinary provider-request attempt reconstructed from a durable step boundary. */ -export interface ModelRequestView { - /** Sequence of the step/start event that opened this attempt. */ - startSeq: number - turn: number - step: number - /** Unix epoch ms from step/start. */ - startedAt: number - /** Assistant completion time, or the failed step/end time when no response completed. */ - completedAt: number | null - status: 'running' | 'complete' | 'error' - error?: string - /** Assistant/message sequence when this attempt completed successfully. */ - resultSeq?: number - provenance?: AssistantProvenanceView - requestConfig?: AssistantRequestConfig - usage?: unknown - /** Retry ordinal scheduled after this failed attempt. */ - retry?: number - maxRetries?: number - retryDelayMs?: number -} - /** Send/stop failure surfaced in the input error strip; op picks the user-facing copy (发送失败 vs 停止失败). */ export interface PromptError { op: 'send' | 'stop' @@ -329,11 +227,11 @@ export interface ConversationSnapshot { /** Append-only context generations split at every model-surface replacement. */ contexts?: readonly ConversationContext[] /** Auxiliary compaction requests, including those without an assistant/message surface node. */ - compactionRequests?: readonly CompactionRequestView[] + compactionRequests?: SessionInspectionSnapshot['compactionRequests'] /** Ordinary provider requests, including failed attempts that produced no assistant message. */ - requestAttempts?: readonly ModelRequestView[] + requestAttempts?: SessionInspectionSnapshot['requestAttempts'] /** System-prompt/tool-catalog changes in request order. */ - promptChanges?: readonly ConversationPromptChange[] + promptChanges?: SessionInspectionSnapshot['promptChanges'] /** Fold degradation flag (cross-window replace defense): when true, nodes come from the lenient linear scan. */ foldDegraded: boolean partial: PartialAssistant | null @@ -346,7 +244,7 @@ export interface ConversationSnapshot { */ codeDispatches: ReadonlyMap /** Model-visible tool schema captured for each recorded call id. */ - callSchemas?: ReadonlyMap + callSchemas?: SessionInspectionSnapshot['callSchemas'] pending: readonly PendingInteraction[] /** Read-only inbox mirror (session/queued frames + mux-open baseline; cleared by the leave-running flip). */ queue: readonly QueuedMessage[] diff --git a/packages/client/runtime/src/client/sessions/fold-adapter.ts b/packages/client/runtime/src/client/sessions/fold-adapter.ts index 3caf00e400..a9d0fc373b 100644 --- a/packages/client/runtime/src/client/sessions/fold-adapter.ts +++ b/packages/client/runtime/src/client/sessions/fold-adapter.ts @@ -11,11 +11,12 @@ import { SurfaceManager, isSurfaceEligibleType, isSurfaceEvent, } from '@deepseek-ai/dsh-session/surface' import type { ToolCallView, ToolEventView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client' -import type { - AssistantRequestConfig, AssistantTiming, ConversationContext, ConversationContextOriginKind, ConversationNode, - ConversationPromptSnapshot, -} from './conversation.ts' +import type { AssistantTiming, ConversationNode } from './conversation.ts' import { toAssistantBlocks } from './conversation.ts' +import type { + AssistantRequestConfig, ConversationContext, ConversationContextOriginKind, + ConversationPromptSnapshot, +} from './inspection.ts' /** In-window tool/call index entry (result-card backfill + runningCalls material). */ export interface CallIndexEntry { @@ -37,6 +38,43 @@ function paddingEvent(seq: number): SessionEvent { return { type: 'noop/padding', seq, time: 0, data: {} } as unknown as SessionEvent } +/** Minimal generation projection owned by the inspection adapter, not the core live surface. */ +interface FoldedContext { + generation: number + nodes: readonly number[] + originSeq?: number +} + +/** + * Replay surface replacements into frozen generations while keeping replacement + * validation and mutation in the canonical core manager. + */ +function foldContexts(events: readonly SessionEvent[]): readonly FoldedContext[] { + const replay: SessionEvent[] = [] + const surface = new SurfaceManager(replay) + const contexts: FoldedContext[] = [] + let generation = 0 + let originSeq: number | undefined + for (const event of events) { + if (isSurfaceEvent(event) && event.surfaceOp !== 'append') { + contexts.push({ + generation, + nodes: [...surface.nodes], + ...(originSeq === undefined ? {} : { originSeq }), + }) + generation++ + originSeq = event.seq + } + replay.push(event) + } + contexts.push({ + generation, + nodes: [...surface.nodes], + ...(originSeq === undefined ? {} : { originSeq }), + }) + return contexts +} + /** One event -> UI node (pure function; the six-variant ConversationNode union). */ function materializeNode( event: SessionEvent, @@ -223,26 +261,26 @@ export class FoldAdapter { this.contextsResult = { rev: this.contextRev, value } return value } - const value = this.surface.contexts.map((context): ConversationContext => { + const value = foldContexts(this.padded).map((context): ConversationContext => { const nodes: ConversationNode[] = [] for (const seq of context.nodes) { const node = this.materialize(seq) if (node !== undefined) nodes.push(node) } const prompt = this.promptsByContext.get(context.generation) - if (context.origin === undefined) { + if (context.originSeq === undefined) { return { id: context.generation, ...(prompt === undefined ? {} : { prompt }), nodes, } } - const originEvent = this.padded[context.origin.seq] + const originEvent = this.padded[context.originSeq] return { id: context.generation, parentId: context.generation - 1, origin: contextOriginKind(originEvent), - originSeq: context.origin.seq, + originSeq: context.originSeq, ...(originEvent === undefined ? {} : { createdAt: originEvent.time }), ...(prompt === undefined ? {} : { prompt }), nodes, diff --git a/packages/client/runtime/src/client/sessions/inspection.ts b/packages/client/runtime/src/client/sessions/inspection.ts new file mode 100644 index 0000000000..ca84914a8c --- /dev/null +++ b/packages/client/runtime/src/client/sessions/inspection.ts @@ -0,0 +1,493 @@ +// Session inspection read models. These projections preserve durable request +// and prompt semantics for diagnostic UIs without making the conversation fold +// or the core SessionSurface own inspection-only history. + +import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm/types' +import type { SessionEvent } from '@deepseek-ai/dsh-session/types' +import type { ConversationNode } from './conversation.ts' + +/** Request configuration recorded in the effective header for one assistant response. */ +export interface AssistantRequestConfig { + provider: string + model: string + purpose?: string + thinking?: string + reasoningEffort?: string + temperature?: number + maxTokens?: number + stop?: readonly string[] +} + +/** Stable provider/model identity attached to one assistant response. */ +export interface AssistantProvenanceView { + provider: string + model: string +} + +/** Operation that started a new append-only model context. */ +export type ConversationContextOriginKind = 'compaction' | 'rewind' | 'rewrite' + +/** Latest complete model request header in force within one context generation. */ +export interface ConversationPromptSnapshot { + /** Provider/model and sampling configuration from the latest effective request header. */ + config?: AssistantRequestConfig + /** Rendered system prompt text; empty when the request had no system prompt. */ + system: string + /** Complete tool catalog sent with the request, including tools that were never called. */ + tools: readonly ToolSchema[] +} + +/** One system-prompt/tool-catalog state that became effective in the request timeline. */ +export interface ConversationPromptChange { + /** Sequence of the request/header event that introduced this state. */ + seq: number + /** Unix epoch ms from the request/header event. */ + time: number + /** How the model-visible system configuration differs from the prior recorded state. */ + kind: 'initial' | 'system' | 'tools' | 'system-and-tools' + /** Complete state effective from this event onward. */ + prompt: ConversationPromptSnapshot + /** State immediately before this change; absent for the initial header. */ + previous?: ConversationPromptSnapshot +} + +/** One immutable model-context generation reconstructed from surface replacements. */ +export interface ConversationContext { + /** Zero-based generation within the session; stable across later appends. */ + id: number + /** Previous generation in this session; absent for the initial context. */ + parentId?: number + /** Why this generation exists; absent for the initial context. */ + origin?: ConversationContextOriginKind + /** Event seq of the replacement that created this generation. */ + originSeq?: number + /** Unix epoch ms of the replacement that created this generation. */ + createdAt?: number + /** Latest request header observed in this generation, inherited until a later header replaces it. */ + prompt?: ConversationPromptSnapshot + /** Final frozen nodes for historical generations, or current folded nodes for the tail. */ + nodes: readonly ConversationNode[] +} + +/** One auxiliary compaction model request reconstructed from its durable lifecycle events. */ +export interface CompactionRequestView { + startSeq: number + turn: number + startedAt: number + completedAt: number | null + status: 'running' | 'complete' | 'error' + error?: string + summarySeq?: number + replacementSeq?: number + summary?: readonly ContentBlock[] + rawOutput?: readonly ContentBlock[] + provenance?: AssistantProvenanceView + requestConfig?: AssistantRequestConfig + usage?: unknown +} + +/** One ordinary provider-request attempt reconstructed from a durable step boundary. */ +export interface ModelRequestView { + /** Sequence of the step/start event that opened this attempt. */ + startSeq: number + turn: number + step: number + /** Unix epoch ms from step/start. */ + startedAt: number + /** Assistant completion time, or the failed step/end time when no response completed. */ + completedAt: number | null + status: 'running' | 'complete' | 'error' + error?: string + /** Assistant/message sequence when this attempt completed successfully. */ + resultSeq?: number + provenance?: AssistantProvenanceView + requestConfig?: AssistantRequestConfig + usage?: unknown + /** Retry ordinal scheduled after this failed attempt. */ + retry?: number + maxRetries?: number + retryDelayMs?: number +} + +/** Stable inspection substructures attached to a conversation snapshot. */ +export interface SessionInspectionSnapshot { + compactionRequests: readonly CompactionRequestView[] + requestAttempts: readonly ModelRequestView[] + promptChanges: readonly ConversationPromptChange[] + callSchemas: ReadonlyMap +} + +interface RetryEvent { + type: 'llm/retry' + seq: number + time: number + data: { + turn: number + step: number + retry: number + maxRetries: number + delayMs: number + failure: { message: string } + } +} + +interface CompactionStartEvent { + type: 'compact/start' + seq: number + time: number + data: { turn: number } +} + +interface CompactionSummaryEvent { + type: 'compact/summary' + seq: number + time: number + data: { + summary: readonly ContentBlock[] + rawOutput?: readonly ContentBlock[] + provider: string + model: string + maxTokens?: number + usage?: unknown + } +} + +interface CompactionEndEvent { + type: 'compact/end' + seq: number + time: number + data: { turn: number; error?: string } +} + +/** + * Own incremental invalidation and call-time schema capture for inspection + * read models. Conversation state delegates events here but owns no request + * reconstruction details. + */ +export class SessionInspection { + private activeToolSchemas = new Map() + private callSchemas = new Map() + private callSchemasRev = 0 + private callSchemasCache: { rev: number; value: ReadonlyMap } | null = null + private modelRequestsRev = 0 + private modelRequestsCache: { + rev: number + value: readonly ModelRequestView[] + } | null = null + private compactionRequestsRev = 0 + private compactionRequestsCache: { + rev: number + value: readonly CompactionRequestView[] + } | null = null + private promptChangesRev = 0 + private promptChangesCache: { + rev: number + value: readonly ConversationPromptChange[] + } | null = null + + /** + * Invalidate projections before replaying a rebuilt history window. + * @returns Nothing. + */ + reset(): void { + this.activeToolSchemas = new Map() + this.callSchemas = new Map() + this.callSchemasRev++ + this.modelRequestsRev++ + this.compactionRequestsRev++ + this.promptChangesRev++ + } + + /** + * Apply inspection-specific incremental state for one durable event. + * @param event - Event entering the current history window. + * @returns Nothing. + */ + applyEvent(event: SessionEvent): void { + if (affectsModelRequests(event)) this.modelRequestsRev++ + if (affectsCompactionRequests(event)) this.compactionRequestsRev++ + if (event.type === 'request/header') { + this.promptChangesRev++ + this.activeToolSchemas = new Map( + (event.data.header.tools ?? []).map(schema => [schema.name, schema]), + ) + return + } + if (event.type === 'tool/call') { + this.captureCallSchema(String(event.data.callId), event.data.name) + } + } + + /** + * Preserve the schema active when one native or nested call starts. + * @param callId - Durable or synthetic call identifier. + * @param name - Tool name used to resolve the active catalog entry. + * @returns Nothing. + */ + captureCallSchema(callId: string, name: string): void { + if (this.callSchemas.has(callId)) return + const schema = this.activeToolSchemas.get(name) + if (schema === undefined) return + this.callSchemas.set(callId, schema) + this.callSchemasRev++ + } + + /** + * Materialize reference-stable inspection projections for the current log. + * @param events - Current contiguous client history window. + * @returns Inspection projections with stable unchanged substructure references. + */ + snapshot(events: readonly SessionEvent[]): SessionInspectionSnapshot { + if (this.callSchemasCache === null || this.callSchemasCache.rev !== this.callSchemasRev) { + this.callSchemasCache = { rev: this.callSchemasRev, value: new Map(this.callSchemas) } + } + if ( + this.modelRequestsCache === null + || this.modelRequestsCache.rev !== this.modelRequestsRev + ) { + this.modelRequestsCache = { + rev: this.modelRequestsRev, + value: deriveModelRequests(events), + } + } + if ( + this.compactionRequestsCache === null + || this.compactionRequestsCache.rev !== this.compactionRequestsRev + ) { + this.compactionRequestsCache = { + rev: this.compactionRequestsRev, + value: deriveCompactionRequests(events), + } + } + if ( + this.promptChangesCache === null + || this.promptChangesCache.rev !== this.promptChangesRev + ) { + this.promptChangesCache = { + rev: this.promptChangesRev, + value: derivePromptChanges(events), + } + } + return { + callSchemas: this.callSchemasCache.value, + requestAttempts: this.modelRequestsCache.value, + compactionRequests: this.compactionRequestsCache.value, + promptChanges: this.promptChangesCache.value, + } + } +} + +function modelRequestKey(turn: number, step: number): string { + return `${turn}\u0000${step}` +} + +function affectsModelRequests(event: SessionEvent): boolean { + switch (event.type) { + case 'request/header': + case 'step/start': + case 'assistant/message': + case 'step/end': + return true + case 'turn/end': + return event.data.reason.kind === 'error' + default: + return (event.type as string) === 'llm/retry' + } +} + +function affectsCompactionRequests(event: SessionEvent): boolean { + const type = event.type as string + return type === 'compact/start' + || type === 'compact/summary' + || type === 'compact/end' + || (event.type === 'user/message' && isCompactionSource(event.data.source)) +} + +/** Project every durable step into one provider request, retaining failed retry attempts. */ +function deriveModelRequests(events: readonly SessionEvent[]): readonly ModelRequestView[] { + const requests: ModelRequestView[] = [] + const byStep = new Map() + let activeStep: string | undefined + let activeConfig: ConversationPromptSnapshot['config'] + + const update = (key: string, change: Partial): void => { + const index = byStep.get(key) + if (index === undefined) return + const request = requests[index] + if (request !== undefined) requests[index] = { ...request, ...change } + } + + for (const sourceEvent of events) { + if (sourceEvent.type === 'request/header') { + activeConfig = sourceEvent.data.header.config + if (activeStep !== undefined) update(activeStep, { requestConfig: activeConfig }) + continue + } + if (sourceEvent.type === 'step/start') { + const { turn, step } = sourceEvent.data + const key = modelRequestKey(turn, step) + byStep.set(key, requests.length) + requests.push({ + startSeq: sourceEvent.seq, + turn, + step, + startedAt: sourceEvent.time, + completedAt: null, + status: 'running', + ...(activeConfig === undefined ? {} : { requestConfig: activeConfig }), + }) + activeStep = key + continue + } + if (sourceEvent.type === 'assistant/message') { + const key = modelRequestKey(sourceEvent.data.turn, sourceEvent.data.step) + update(key, { + completedAt: sourceEvent.time, + status: 'complete', + resultSeq: sourceEvent.seq, + provenance: { + provider: sourceEvent.data.provenance.provider, + model: sourceEvent.data.provenance.model, + }, + ...(sourceEvent.data.usage === undefined ? {} : { usage: sourceEvent.data.usage }), + }) + continue + } + if (sourceEvent.type === 'step/end') { + const key = modelRequestKey(sourceEvent.data.turn, sourceEvent.data.step) + const index = byStep.get(key) + const request = index === undefined ? undefined : requests[index] + if (index !== undefined && request !== undefined && request.status === 'running') { + requests[index] = { + ...request, + completedAt: sourceEvent.time, + status: 'error', + } + } + if (activeStep === key) activeStep = undefined + continue + } + if ((sourceEvent.type as string) === 'llm/retry') { + const event = sourceEvent as unknown as RetryEvent + update(modelRequestKey(event.data.turn, event.data.step), { + status: 'error', + error: event.data.failure.message, + retry: event.data.retry, + maxRetries: event.data.maxRetries, + retryDelayMs: event.data.delayMs, + }) + continue + } + if (sourceEvent.type !== 'turn/end' || sourceEvent.data.reason.kind !== 'error') continue + const reason = sourceEvent.data.reason + update(modelRequestKey(sourceEvent.data.turn, reason.step), { + status: 'error', + error: 'failure' in reason ? reason.failure.message : reason.message, + }) + } + return requests +} + +/** Project log-only compaction request brackets without coupling the client runtime to one backend package. */ +function deriveCompactionRequests(events: readonly SessionEvent[]): readonly CompactionRequestView[] { + const requests: CompactionRequestView[] = [] + let active: CompactionRequestView | undefined + for (const sourceEvent of events) { + const type = sourceEvent.type as string + if (type === 'compact/start') { + const event = sourceEvent as unknown as CompactionStartEvent + active = { + startSeq: event.seq, + turn: event.data.turn, + startedAt: event.time, + completedAt: null, + status: 'running', + } + continue + } + if (type === 'compact/summary' && active !== undefined) { + const event = sourceEvent as unknown as CompactionSummaryEvent + active = { + ...active, + summarySeq: event.seq, + summary: event.data.summary, + ...(event.data.rawOutput === undefined ? {} : { rawOutput: event.data.rawOutput }), + provenance: { + provider: event.data.provider, + model: event.data.model, + }, + requestConfig: { + provider: event.data.provider, + model: event.data.model, + purpose: 'compaction', + ...(event.data.maxTokens === undefined ? {} : { maxTokens: event.data.maxTokens }), + }, + ...(event.data.usage === undefined ? {} : { usage: event.data.usage }), + } + continue + } + if ( + sourceEvent.type === 'user/message' + && active?.summarySeq !== undefined + && isCompactionSource(sourceEvent.data.source) + ) { + active = { ...active, replacementSeq: sourceEvent.seq } + continue + } + if (type !== 'compact/end' || active === undefined) continue + const event = sourceEvent as unknown as CompactionEndEvent + active = { + ...active, + completedAt: event.time, + status: event.data.error === undefined ? 'complete' : 'error', + ...(event.data.error === undefined ? {} : { error: event.data.error }), + } + requests.push(active) + active = undefined + } + if (active !== undefined) requests.push(active) + return requests +} + +/** Project request headers into model-visible system/tool changes only. */ +function derivePromptChanges(events: readonly SessionEvent[]): readonly ConversationPromptChange[] { + const changes: ConversationPromptChange[] = [] + let previous: ConversationPromptSnapshot | undefined + for (const event of events) { + if (event.type !== 'request/header') continue + const prompt: ConversationPromptSnapshot = { + config: event.data.header.config, + system: event.data.header.system ?? '', + tools: event.data.header.tools ?? [], + } + const systemChanged = previous !== undefined && previous.system !== prompt.system + const toolsChanged = previous !== undefined + && JSON.stringify(previous.tools) !== JSON.stringify(prompt.tools) + if (previous === undefined || systemChanged || toolsChanged) { + changes.push({ + seq: event.seq, + time: event.time, + kind: previous === undefined + ? 'initial' + : systemChanged && toolsChanged + ? 'system-and-tools' + : systemChanged + ? 'system' + : 'tools', + prompt, + ...(previous === undefined ? {} : { previous }), + }) + } + previous = prompt + } + return changes +} + +function isCompactionSource(source: unknown): boolean { + return typeof source === 'object' + && source !== null + && 'kind' in source + && source.kind === 'plugin' + && 'plugin' in source + && source.plugin === 'compact' +} diff --git a/packages/client/runtime/src/client/sessions/session.ts b/packages/client/runtime/src/client/sessions/session.ts index dbe4f3bb6b..e212041880 100644 --- a/packages/client/runtime/src/client/sessions/session.ts +++ b/packages/client/runtime/src/client/sessions/session.ts @@ -1,7 +1,7 @@ // Sessions remain resident after creation so they continue consuming mux frames off-screen. import type { Context } from 'cordis' -import type { ContentBlock, ToolSchema } from '@deepseek-ai/dsh-llm/types' +import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' import type { SessionEvent, TodoItem } from '@deepseek-ai/dsh-session/types' import type { HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult, @@ -12,10 +12,10 @@ import type { import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api' import type { ObservableSnapshot } from '../contract/store.ts' import type { - CodeSubCall, CompactionRequestView, ComposerPhase, ConversationNode, - ConversationPromptChange, ConversationPromptSnapshot, ConversationSnapshot, - ModelRequestView, OpenState, PromptError, QueuedMessage, RunningToolCall, + CodeSubCall, ComposerPhase, ConversationNode, ConversationSnapshot, + OpenState, PromptError, QueuedMessage, RunningToolCall, } from './conversation.ts' +import { SessionInspection } from './inspection.ts' import type { PendingInteraction } from './pending.ts' import { PendingWait } from './pending.ts' import { FoldAdapter } from './fold-adapter.ts' @@ -108,27 +108,8 @@ export class Session implements ObservableSnapshot { private codeDispatches = new Map() private dispatchesRev = 0 private dispatchesCache: { rev: number; value: ReadonlyMap } | null = null - /** Schemas in force for the next tool/call, updated by request/header. */ - private activeToolSchemas = new Map() - /** Call-time schema snapshots keyed by native or code-dispatch call id. */ - private callSchemas = new Map() - private callSchemasRev = 0 - private callSchemasCache: { rev: number; value: ReadonlyMap } | null = null - private modelRequestsRev = 0 - private modelRequestsCache: { - rev: number - value: readonly ModelRequestView[] - } | null = null - private compactionRequestsRev = 0 - private compactionRequestsCache: { - rev: number - value: readonly CompactionRequestView[] - } | null = null - private promptChangesRev = 0 - private promptChangesCache: { - rev: number - value: readonly ConversationPromptChange[] - } | null = null + /** Diagnostic projections isolated from the ordinary conversation state machine. */ + private readonly inspection = new SessionInspection() private running = false /** * Sticky send marker, private input of the composerPhase derivation: set @@ -624,8 +605,7 @@ export class Session implements ObservableSnapshot { /** Per-event side effects (right column of the §A.9 dispatch table): * chunk accumulation / partial clear on finalize / openCalls add-remove. */ private applyEventSideEffects(event: SessionEvent, view?: ToolEventView): void { - if (affectsModelRequests(event)) this.modelRequestsRev++ - if (affectsCompactionRequests(event)) this.compactionRequestsRev++ + this.inspection.applyEvent(event) // The `tool/code-dispatch-start`/`tool/code-dispatch` pair is declared by // the host-side dsh-tools plugin whose types cannot enter the client // program (its host Context merges collide with the client's), so this @@ -646,7 +626,7 @@ export class Session implements ObservableSnapshot { argsRaw: JSON.stringify(data.arguments), turn: 0, step: 0, time: event.time, callView: null, } - this.captureCallSchema(data.subCallId, data.name) + this.inspection.captureCallSchema(data.subCallId, data.name) const siblings = this.codeDispatches.get(data.parentCallId) ?? [] this.codeDispatches.set(data.parentCallId, [...siblings, running]) this.dispatchesRev++ @@ -666,7 +646,7 @@ export class Session implements ObservableSnapshot { content: ContentBlock[] } const siblings = this.codeDispatches.get(data.parentCallId) ?? [] - this.captureCallSchema(data.subCallId, data.name) + this.inspection.captureCallSchema(data.subCallId, data.name) const at = siblings.findIndex(sub => sub.callId === data.subCallId) const started = at === -1 ? undefined : siblings[at] const settled: CodeSubCall = { @@ -688,13 +668,6 @@ export class Session implements ObservableSnapshot { return } switch (event.type) { - case 'request/header': { - this.promptChangesRev++ - this.activeToolSchemas = new Map( - (event.data.header.tools ?? []).map(schema => [schema.name, schema]), - ) - return - } case 'assistant/chunk': { const { turn, step, chunk } = event.data if (this.partial === null || this.partial.turn !== turn || this.partial.step !== step) { @@ -710,7 +683,6 @@ export class Session implements ObservableSnapshot { return } case 'tool/call': { - this.captureCallSchema(String(event.data.callId), event.data.name) this.openCalls.set(String(event.data.callId), { callId: String(event.data.callId), name: event.data.name, argsRaw: event.data.arguments, turn: event.data.turn, step: event.data.step, time: event.time, @@ -769,15 +741,6 @@ export class Session implements ObservableSnapshot { } } - /** Preserve the schema active when one call starts. */ - private captureCallSchema(callId: string, name: string): void { - if (this.callSchemas.has(callId)) return - const schema = this.activeToolSchemas.get(name) - if (schema === undefined) return - this.callSchemas.set(callId, schema) - this.callSchemasRev++ - } - /** Re-derive state (partial/openCalls/frozenNodes) from raw window events after a rebuild — keeps * paging/stitching consistent, and makes the live freeze and the history replay converge on the * same interrupted nodes (chunks are logged, so the replayed sweep re-freezes identical text). @@ -792,12 +755,7 @@ export class Session implements ObservableSnapshot { this.frozenRev++ this.codeDispatches = new Map() this.dispatchesRev++ - this.activeToolSchemas = new Map() - this.callSchemas = new Map() - this.callSchemasRev++ - this.modelRequestsRev++ - this.compactionRequestsRev++ - this.promptChangesRev++ + this.inspection.reset() for (let i = 0; i < this.events.length; i++) { const event = this.events[i] /* v8 ignore next -- dense-array guard: i stays within events.length, so the undefined arm needs a sparse array no caller builds. */ @@ -834,53 +792,24 @@ export class Session implements ObservableSnapshot { if (this.dispatchesCache === null || this.dispatchesCache.rev !== this.dispatchesRev) { this.dispatchesCache = { rev: this.dispatchesRev, value: new Map(this.codeDispatches) } } - if (this.callSchemasCache === null || this.callSchemasCache.rev !== this.callSchemasRev) { - this.callSchemasCache = { rev: this.callSchemasRev, value: new Map(this.callSchemas) } - } - if ( - this.modelRequestsCache === null - || this.modelRequestsCache.rev !== this.modelRequestsRev - ) { - this.modelRequestsCache = { - rev: this.modelRequestsRev, - value: deriveModelRequests(this.events), - } - } - if ( - this.compactionRequestsCache === null - || this.compactionRequestsCache.rev !== this.compactionRequestsRev - ) { - this.compactionRequestsCache = { - rev: this.compactionRequestsRev, - value: deriveCompactionRequests(this.events), - } - } if (this.queueCache === null || this.queueCache.rev !== this.queueRev) { this.queueCache = { rev: this.queueRev, value: this.queued.map(entry => entry.row) } } - if ( - this.promptChangesCache === null - || this.promptChangesCache.rev !== this.promptChangesRev - ) { - this.promptChangesCache = { - rev: this.promptChangesRev, - value: derivePromptChanges(this.events), - } - } + const inspection = this.inspection.snapshot(this.events) const partial = this.partial?.toPartial() ?? null return { sessionId: this.sessionId, nodes, contexts, - compactionRequests: this.compactionRequestsCache.value, - requestAttempts: this.modelRequestsCache.value, - promptChanges: this.promptChangesCache.value, + compactionRequests: inspection.compactionRequests, + requestAttempts: inspection.requestAttempts, + promptChanges: inspection.promptChanges, foldDegraded: degraded, partial, runningCalls: this.callsCache.value, pending: this.pendingCache.value, codeDispatches: this.dispatchesCache.value, - callSchemas: this.callSchemasCache.value, + callSchemas: inspection.callSchemas, queue: this.queueCache.value, running: this.running, composerPhase: derivePhase( @@ -914,260 +843,3 @@ function derivePhase(hasContent: boolean, promptAttempted: boolean): ComposerPha if (hasContent) return 'active' return promptAttempted ? 'engaging' : 'blank' } - -interface RetryEvent { - type: 'llm/retry' - seq: number - time: number - data: { - turn: number - step: number - retry: number - maxRetries: number - delayMs: number - failure: { message: string } - } -} - -function modelRequestKey(turn: number, step: number): string { - return `${turn}\u0000${step}` -} - -function affectsModelRequests(event: SessionEvent): boolean { - switch (event.type) { - case 'request/header': - case 'step/start': - case 'assistant/message': - case 'step/end': - return true - case 'turn/end': - return event.data.reason.kind === 'error' - default: - return (event.type as string) === 'llm/retry' - } -} - -function affectsCompactionRequests(event: SessionEvent): boolean { - const type = event.type as string - return type === 'compact/start' - || type === 'compact/summary' - || type === 'compact/end' - || (event.type === 'user/message' && isCompactionSource(event.data.source)) -} - -/** Project every durable step into one provider request, retaining failed retry attempts. */ -function deriveModelRequests(events: readonly SessionEvent[]): readonly ModelRequestView[] { - const requests: ModelRequestView[] = [] - const byStep = new Map() - let activeStep: string | undefined - let activeConfig: ConversationPromptSnapshot['config'] - - const update = (key: string, change: Partial): void => { - const index = byStep.get(key) - if (index === undefined) return - const request = requests[index] - if (request !== undefined) requests[index] = { ...request, ...change } - } - - for (const sourceEvent of events) { - if (sourceEvent.type === 'request/header') { - activeConfig = sourceEvent.data.header.config - if (activeStep !== undefined) update(activeStep, { requestConfig: activeConfig }) - continue - } - if (sourceEvent.type === 'step/start') { - const { turn, step } = sourceEvent.data - const key = modelRequestKey(turn, step) - byStep.set(key, requests.length) - requests.push({ - startSeq: sourceEvent.seq, - turn, - step, - startedAt: sourceEvent.time, - completedAt: null, - status: 'running', - ...(activeConfig === undefined ? {} : { requestConfig: activeConfig }), - }) - activeStep = key - continue - } - if (sourceEvent.type === 'assistant/message') { - const key = modelRequestKey(sourceEvent.data.turn, sourceEvent.data.step) - update(key, { - completedAt: sourceEvent.time, - status: 'complete', - resultSeq: sourceEvent.seq, - provenance: { - provider: sourceEvent.data.provenance.provider, - model: sourceEvent.data.provenance.model, - }, - ...(sourceEvent.data.usage === undefined ? {} : { usage: sourceEvent.data.usage }), - }) - continue - } - if (sourceEvent.type === 'step/end') { - const key = modelRequestKey(sourceEvent.data.turn, sourceEvent.data.step) - const index = byStep.get(key) - const request = index === undefined ? undefined : requests[index] - if (index !== undefined && request !== undefined && request.status === 'running') { - requests[index] = { - ...request, - completedAt: sourceEvent.time, - status: 'error', - } - } - if (activeStep === key) activeStep = undefined - continue - } - if ((sourceEvent.type as string) === 'llm/retry') { - const event = sourceEvent as unknown as RetryEvent - update(modelRequestKey(event.data.turn, event.data.step), { - status: 'error', - error: event.data.failure.message, - retry: event.data.retry, - maxRetries: event.data.maxRetries, - retryDelayMs: event.data.delayMs, - }) - continue - } - if (sourceEvent.type !== 'turn/end' || sourceEvent.data.reason.kind !== 'error') continue - const reason = sourceEvent.data.reason - update(modelRequestKey(sourceEvent.data.turn, reason.step), { - status: 'error', - error: 'failure' in reason ? reason.failure.message : reason.message, - }) - } - return requests -} - -interface CompactionStartEvent { - type: 'compact/start' - seq: number - time: number - data: { turn: number } -} - -interface CompactionSummaryEvent { - type: 'compact/summary' - seq: number - time: number - data: { - summary: readonly ContentBlock[] - rawOutput?: readonly ContentBlock[] - provider: string - model: string - maxTokens?: number - usage?: unknown - } -} - -interface CompactionEndEvent { - type: 'compact/end' - seq: number - time: number - data: { turn: number; error?: string } -} - -/** Project log-only compaction request brackets without coupling the client runtime to one backend package. */ -function deriveCompactionRequests(events: readonly SessionEvent[]): readonly CompactionRequestView[] { - const requests: CompactionRequestView[] = [] - let active: CompactionRequestView | undefined - for (const sourceEvent of events) { - const type = sourceEvent.type as string - if (type === 'compact/start') { - const event = sourceEvent as unknown as CompactionStartEvent - active = { - startSeq: event.seq, - turn: event.data.turn, - startedAt: event.time, - completedAt: null, - status: 'running', - } - continue - } - if (type === 'compact/summary' && active !== undefined) { - const event = sourceEvent as unknown as CompactionSummaryEvent - active = { - ...active, - summarySeq: event.seq, - summary: event.data.summary, - ...(event.data.rawOutput === undefined ? {} : { rawOutput: event.data.rawOutput }), - provenance: { - provider: event.data.provider, - model: event.data.model, - }, - requestConfig: { - provider: event.data.provider, - model: event.data.model, - purpose: 'compaction', - ...(event.data.maxTokens === undefined ? {} : { maxTokens: event.data.maxTokens }), - }, - ...(event.data.usage === undefined ? {} : { usage: event.data.usage }), - } - continue - } - if ( - sourceEvent.type === 'user/message' - && active?.summarySeq !== undefined - && isCompactionSource(sourceEvent.data.source) - ) { - active = { ...active, replacementSeq: sourceEvent.seq } - continue - } - if (type !== 'compact/end' || active === undefined) continue - const event = sourceEvent as unknown as CompactionEndEvent - active = { - ...active, - completedAt: event.time, - status: event.data.error === undefined ? 'complete' : 'error', - ...(event.data.error === undefined ? {} : { error: event.data.error }), - } - requests.push(active) - active = undefined - } - if (active !== undefined) requests.push(active) - return requests -} - -/** Project request headers into model-visible system/tool changes only. */ -function derivePromptChanges(events: readonly SessionEvent[]): readonly ConversationPromptChange[] { - const changes: ConversationPromptChange[] = [] - let previous: ConversationPromptSnapshot | undefined - for (const event of events) { - if (event.type !== 'request/header') continue - const prompt: ConversationPromptSnapshot = { - config: event.data.header.config, - system: event.data.header.system ?? '', - tools: event.data.header.tools ?? [], - } - const systemChanged = previous !== undefined && previous.system !== prompt.system - const toolsChanged = previous !== undefined - && JSON.stringify(previous.tools) !== JSON.stringify(prompt.tools) - if (previous === undefined || systemChanged || toolsChanged) { - changes.push({ - seq: event.seq, - time: event.time, - kind: previous === undefined - ? 'initial' - : systemChanged && toolsChanged - ? 'system-and-tools' - : systemChanged - ? 'system' - : 'tools', - prompt, - ...(previous === undefined ? {} : { previous }), - }) - } - previous = prompt - } - return changes -} - -function isCompactionSource(source: unknown): boolean { - return typeof source === 'object' - && source !== null - && 'kind' in source - && source.kind === 'plugin' - && 'plugin' in source - && source.plugin === 'compact' -} diff --git a/packages/client/runtime/tests/fold-adapter.spec.ts b/packages/client/runtime/tests/fold-adapter.spec.ts index bb360e2a67..9e20b07fae 100644 --- a/packages/client/runtime/tests/fold-adapter.spec.ts +++ b/packages/client/runtime/tests/fold-adapter.spec.ts @@ -34,6 +34,47 @@ describe('FoldAdapter', () => { expect(second.nodes).not.toBe(first.nodes) // array itself fresh per call }) + it('projects frozen surface generations without widening the core live surface', () => { + const adapter = new FoldAdapter() + adapter.reset([ + ev.user(0, 'a'), + ev.user(1, 'b'), + at(2, { + type: 'assistant/message', + surfaceOp: { op: 'replace', start: 0, end: 0 }, + sourceEventSeqs: [0], + data: { + turn: 1, + step: 1, + content: [{ type: 'text', text: 'summary' }], + provenance: { provider: 'fake', model: 'fake' }, + }, + }), + at(3, { + type: 'assistant/message', + surfaceOp: { op: 'replace', start: 2, end: 1 }, + sourceEventSeqs: [2, 1], + data: { + turn: 1, + step: 2, + content: [{ type: 'text', text: 'summary 2' }], + provenance: { provider: 'fake', model: 'fake' }, + }, + }), + ], 0) + + expect(adapter.contexts().map(context => ({ + id: context.id, + parentId: context.parentId, + originSeq: context.originSeq, + nodes: context.nodes.map(node => node.seq), + }))).toEqual([ + { id: 0, parentId: undefined, originSeq: undefined, nodes: [0, 1] }, + { id: 1, parentId: 0, originSeq: 2, nodes: [2, 1] }, + { id: 2, parentId: 1, originSeq: 3, nodes: [3] }, + ]) + }) + it('materializes all six node variants with field mapping', () => { const adapter = new FoldAdapter() const events = [ diff --git a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx index 5751471ae5..93b7bf1ba0 100644 --- a/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx +++ b/packages/client/ui-trajectory/src/client/TrajectoryTable.tsx @@ -591,12 +591,9 @@ function messageOriginLabel(source: unknown): string { function MessageOrigin({ record }: { record: TableRecord }) { const source = record.cell.messageSource if (source === undefined) return

Origin not recorded

- const sourceRoot = typeof source === 'object' && source !== null + const data = typeof source === 'object' && source !== null ? source : { value: source } - const data = record.cell.messageMeta === undefined - ? sourceRoot - : { source, meta: record.cell.messageMeta } return ( { /** Producer provenance from a user-role message or context injection. */ messageSource?: unknown /** Producer-owned model-hidden metadata carried beside the message source. */ - messageMeta?: unknown /** A separator-only anchor for an auxiliary request with no visible record. */ requestOnly?: boolean /** Full request/message content for the details panel. */ diff --git a/packages/compact/compact-basic/src/region.ts b/packages/compact/compact-basic/src/region.ts index ec21c02218..277f765836 100644 --- a/packages/compact/compact-basic/src/region.ts +++ b/packages/compact/compact-basic/src/region.ts @@ -168,7 +168,6 @@ export async function compactSurfaceRegion( summarySeq: summaryEvent.seq, endSeq: endEvent.seq, summary, - ...rawOutput === undefined ? {} : { rawOutput }, shadowedRange: { start, end }, shadowedSeqs, shadowedTokenCount, diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index cebea61c66..276fbda5c9 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -816,7 +816,6 @@ describe('compaction region transaction', () => { expect(result.shadowedSeqs).toEqual(before.slice(0, 4)) expect(result.shadowedTokenCount).toBeGreaterThan(0) - expect(result.rawOutput).toEqual(compact.rawOutput) expect(compact.calls[0]).toMatchObject({ signal: SIGNAL }) expect(summarizedText(compact.calls[0]!.input)).toContain('fixture user 1') const summary = session.events.findLast(event => event.type === 'compact/summary') diff --git a/packages/compact/compact/src/types.ts b/packages/compact/compact/src/types.ts index 67b49d365c..b77735b229 100644 --- a/packages/compact/compact/src/types.ts +++ b/packages/compact/compact/src/types.ts @@ -55,8 +55,6 @@ export interface CompactionResult { endSeq: number /** The summary content blocks produced by the backend. */ summary: ContentBlock[] - /** Complete provider output before the backend's safe summary projection. */ - rawOutput?: ContentBlock[] /** * The surface-boundary pair that was shadowed: the seqs of the first * (`start`) and last (`end`) surface nodes of the replaced range. A diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index b9c6bd305b..e3f81aa527 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -1539,7 +1539,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'CompactionResult', - declaration: 'export interface CompactionResult {\n startSeq: number;\n summarySeq: number;\n endSeq: number;\n summary: ContentBlock[];\n rawOutput?: ContentBlock[];\n shadowedRange: {\n start: number;\n end: number;\n };\n shadowedSeqs: number[];\n shadowedTokenCount: number;\n}', + declaration: 'export interface CompactionResult {\n startSeq: number;\n summarySeq: number;\n endSeq: number;\n summary: ContentBlock[];\n shadowedRange: {\n start: number;\n end: number;\n };\n shadowedSeqs: number[];\n shadowedTokenCount: number;\n}', }, { name: 'CompactionTrigger', diff --git a/packages/core/session/src/surface.ts b/packages/core/session/src/surface.ts index dd87d0b703..fc275129f9 100644 --- a/packages/core/session/src/surface.ts +++ b/packages/core/session/src/surface.ts @@ -57,16 +57,6 @@ export interface SurfaceFoldResult { replacements: SurfaceFoldReplacement[] } -/** One append-only surface generation separated from its successor by a replacement. */ -export interface SurfaceFoldContext { - /** Zero-based generation within this session log. */ - generation: number - /** Surface sequences present when this generation froze, or at the current tail. */ - nodes: number[] - /** Replacement operation that created this generation; absent for the initial context. */ - origin?: SurfaceFoldReplacement -} - /** Readonly live projection of the message-producing session events. */ export interface SessionSurface { /** Current surface event sequences in model-visible order. */ @@ -270,11 +260,14 @@ function planSurfaceEvent( } } -/** Commit one validated transition and return replacement metadata when one occurred. */ -function applySurfacePlan( +/** Apply one event and return replacement metadata only when one occurred. */ +function applySurfaceEvent( state: SurfaceFoldState, - plan: SurfacePlan | undefined, + event: SessionEvent, + expectedSeq: number, + events: readonly SessionEvent[], ): SurfaceFoldReplacement | undefined { + const plan = planSurfaceEvent(state, event, expectedSeq, events) if (plan?.kind === 'append') { state.nodes.push(plan.seq) } else if (plan?.kind === 'replace') { @@ -290,16 +283,6 @@ function applySurfacePlan( } } -/** Apply one event and return replacement metadata only when one occurred. */ -function applySurfaceEvent( - state: SurfaceFoldState, - event: SessionEvent, - expectedSeq: number, - events: readonly SessionEvent[], -): SurfaceFoldReplacement | undefined { - return applySurfacePlan(state, planSurfaceEvent(state, event, expectedSeq, events)) -} - /** * Replay a complete session log through the canonical surface fold. * @param events - session events in contiguous seq order. @@ -316,34 +299,9 @@ export function foldSurface(events: readonly SessionEvent[]): SurfaceFoldResult return { nodes: [...state.nodes], replacements } } -/** Reconstruct every surface generation only for consumers that request history. */ -function foldSurfaceContexts(events: readonly SessionEvent[]): SurfaceFoldContext[] { - const state = createFoldState() - const contexts: SurfaceFoldContext[] = [] - let origin: SurfaceFoldReplacement | undefined - for (const [index, event] of events.entries()) { - const plan = planSurfaceEvent(state, event, index, events) - const priorNodes = plan?.kind === 'replace' ? [...state.nodes] : undefined - const replacement = applySurfacePlan(state, plan) - if (replacement === undefined || priorNodes === undefined) continue - contexts.push({ - generation: state.replaceGeneration - 1, - nodes: priorNodes, - ...(origin === undefined ? {} : { origin }), - }) - origin = replacement - } - contexts.push({ - generation: state.replaceGeneration, - nodes: [...state.nodes], - ...(origin === undefined ? {} : { origin }), - }) - return contexts -} - /** Incremental ordered surface view and append-boundary validator. */ export class SurfaceManager implements SessionSurface { - /** Shared transition state for the live surface. */ + /** Shared transition state; replacement history is not retained. */ private _state = createFoldState() /** Last processed seq; -1 folds a seeded log on first access. */ private _lastProcessedSeq = -1 @@ -371,18 +329,11 @@ export class SurfaceManager implements SessionSurface { return this._state.nodes } - /** Surface generations reconstructed on demand without burdening ordinary live sessions. */ - get contexts(): readonly SurfaceFoldContext[] { - if (this._lastProcessedSeq < this.log.length - 1) this._processDelta() - return foldSurfaceContexts(this.log) - } - /** Fold events appended since the previous access. */ 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 - const event = this.log[i]! - applySurfaceEvent(this._state, event, i, this.log) + applySurfaceEvent(this._state, this.log[i]!, i, this.log) this._lastProcessedSeq = i } } diff --git a/packages/core/session/tests/surface.spec.ts b/packages/core/session/tests/surface.spec.ts index e838a9f96d..60e68e18ec 100644 --- a/packages/core/session/tests/surface.spec.ts +++ b/packages/core/session/tests/surface.spec.ts @@ -8,7 +8,6 @@ import { isSurfaceEvent, } from '@deepseek-ai/dsh-session' import { CallId } from '@deepseek-ai/dsh-llm' -import { SurfaceManager } from '../src/surface.ts' /** Build a minimal session with turn boundaries and a single user message. */ function surfaceSession(): Session { @@ -190,19 +189,6 @@ describe('SurfaceManager', () => { { seq: 2, start: 0, end: 0, shadowedSeqs: [0] }, { seq: 3, start: 2, end: 1, shadowedSeqs: [2, 1] }, ]) - expect(new SurfaceManager(s.events).contexts).toEqual([ - { generation: 0, nodes: [0, 1] }, - { - generation: 1, - nodes: [2, 1], - origin: { seq: 2, start: 0, end: 0, shadowedSeqs: [0] }, - }, - { - generation: 2, - nodes: [3], - origin: { seq: 3, start: 2, end: 1, shadowedSeqs: [2, 1] }, - }, - ]) folded.nodes[0] = 99 folded.replacements[0]!.shadowedSeqs.push(99) expect(s.surface.nodes).toEqual([3]) @@ -218,7 +204,6 @@ describe('SurfaceManager', () => { expect(s.surface.nodes).toEqual([1]) const manager = s.surface as unknown as { _state: object } expect(Object.hasOwn(manager._state, 'replacements')).toBe(false) - expect(Object.hasOwn(manager, '_contexts')).toBe(false) expect(foldSurface(s.events).replacements).toEqual([ { seq: 1, start: 0, end: 0, shadowedSeqs: [0] }, ])