Merge remote-tracking branch 'origin/stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts: # packages/client/connection/README.i18n.yaml # packages/client/connection/README.md # packages/client/connection/README.zh.md
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Behavioral half of the compaction-checkpoint drift trap.
|
||||
*
|
||||
* `TranscriptAdapter` pins its plugin literal to the seam's own declaration at
|
||||
* `TranscriptAdapter` pins its plugin literal to the Service Definition's declaration at
|
||||
* compile time through a type-only import of `dsh-compact/checkpoint`, so
|
||||
* renaming the seam's plugin already fails `tsc`. This spec covers the same
|
||||
* renaming the Service Definition's plugin already fails `tsc`. This spec covers the same
|
||||
* drift from the other side — end to end through the adapter, driving it with a
|
||||
* checkpoint built from the canonical `COMPACT_CHECKPOINT_SOURCE` value and
|
||||
* checking the seam's own predicate agrees. Both values come from the
|
||||
* checking the Service Definition's predicate agrees. Both values come from the
|
||||
* cordis-free checkpoint leaf, so the client test program never loads the host
|
||||
* package root or its `Context` merges.
|
||||
*/
|
||||
@@ -17,7 +17,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
|
||||
import { TranscriptAdapter } from '../src/client/sessions/transcript-adapter.ts'
|
||||
|
||||
/** A replacement user message stamped with the seam's own canonical source. */
|
||||
/** A replacement user message stamped with the Service Definition's canonical source. */
|
||||
function canonicalCheckpoint(seq: number): SessionEvent {
|
||||
return {
|
||||
type: 'user/message',
|
||||
@@ -43,7 +43,7 @@ describe('compaction checkpoint recognition', () => {
|
||||
})
|
||||
|
||||
it("agrees with the seam's own predicate on the source it recognizes", () => {
|
||||
// Both sides answer the same question about the same value: if the seam
|
||||
// Both sides answer the same question about the same value: if the Service Definition
|
||||
// renames its plugin, this equality is what breaks.
|
||||
const checkpoint = canonicalCheckpoint(1)
|
||||
expect(checkpoint.type === 'user/message' && isCompactCheckpointSource(checkpoint.data.source)).toBe(true)
|
||||
|
||||
@@ -105,7 +105,7 @@ export const ev = {
|
||||
...text === undefined ? {} : { text },
|
||||
...sourceEventSeq === undefined ? {} : { sourceEventSeq },
|
||||
} }),
|
||||
/** A compaction's log-only `compact/summary` provenance record. */
|
||||
/** A compaction's log-only `compact/summary` record. */
|
||||
compactSummary: (seq: number, summary: string, start: number, end: number): SessionEvent =>
|
||||
at(seq, { type: 'compact/summary', data: {
|
||||
summary: text(summary),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// deferred-controlled timing). Streams are hand pumps: pushMux/pushHost.
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import type {
|
||||
ClientResponse, CommandDescriptor, HostFrame, IApiClient, ModelTarget, MuxFrame,
|
||||
ClientResponse, CommandDescriptor, HostFrame, IApiClient, ModelSelection, MuxFrame,
|
||||
RpcError, RpcReceipt, RpcRequest, RpcResponse, SessionId, SessionModels, SessionSearchItem, SkillEntry,
|
||||
WorkspaceId, WorkspaceView,
|
||||
} from '@deepseek-ai/dsh-client-connection/client'
|
||||
@@ -64,7 +64,7 @@ export class FakeApiClient implements IApiClient {
|
||||
onSearch: (payload: unknown) => Promise<RpcResponse<{ items: SessionSearchItem[]; hasMore: boolean }>> =
|
||||
() => Promise.resolve(ok({ items: [], hasMore: false }))
|
||||
onCreate: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-new' as SessionId }))
|
||||
readonly defaultModel: ModelTarget = { provider: 'deepseek-official', model: 'deepseek-v4-flash' }
|
||||
readonly defaultModel: ModelSelection = { provider: 'deepseek-official', model: 'deepseek-v4-flash' }
|
||||
onRename: (payload: unknown) => Promise<RpcResponse<{ title: string; seq: number }>> = () => Promise.resolve(ok({ title: 'fk-renamed', seq: 0 }))
|
||||
onFork: (payload: unknown) => Promise<RpcResponse<{ sessionId: SessionId }>> = () => Promise.resolve(ok({ sessionId: 'fk-fork' as SessionId }))
|
||||
onHistory: (payload: { sessionId: SessionId; beforeSeq?: number; maxMessages?: number })
|
||||
@@ -82,7 +82,7 @@ export class FakeApiClient implements IApiClient {
|
||||
failures: [],
|
||||
}))
|
||||
onSelectModel: (payload: { provider: string; model: string }) =>
|
||||
Promise<RpcResponse<{ selected: ModelTarget }>> =
|
||||
Promise<RpcResponse<{ selected: ModelSelection }>> =
|
||||
payload => Promise.resolve(ok({ selected: { provider: payload.provider, model: payload.model } }))
|
||||
onPrompt: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
onUpdateQueue: (payload: unknown) => Promise<RpcResponse<{ accepted: true }>> = () => Promise.resolve(ok({ accepted: true as const }))
|
||||
|
||||
@@ -12,7 +12,7 @@ const at = (seq: number, event: Record<string, unknown>): SessionEvent =>
|
||||
describe('projectConversationHistory', () => {
|
||||
it('names an injected context node from its durable source, like the live adapter', () => {
|
||||
// The fold declares its own node mapping (jscpd:ignore in the source), so
|
||||
// the provenance projection is pinned on both sides independently.
|
||||
// the source projection is pinned on both sides independently.
|
||||
const injected = at(0, {
|
||||
type: 'user/message',
|
||||
surfaceOp: 'append',
|
||||
|
||||
@@ -842,7 +842,7 @@ describe('connected generation', () => {
|
||||
api.onHistory = () => Promise.resolve(ok({
|
||||
events: entries(plainTurn(0, 0, 'a', 'b')) as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'deepseek-chat' },
|
||||
}))
|
||||
const manager = new SessionManager(api)
|
||||
const openedSession = manager.get(S1)
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SessionManager } from '../src/client/sessions/manager.ts'
|
||||
import { FakeApiClient, ok } from './fake-api.ts'
|
||||
import { entries, plainTurn } from './event-script.ts'
|
||||
|
||||
// Test-domain keys merged into the projection map (the interface package's
|
||||
// Test-domain keys merged into the projection map (the Service Definition package's
|
||||
// pure-type outlet), the same way domain host plugins merge theirs.
|
||||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||||
interface SessionProjectionMap {
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('open', () => {
|
||||
gate.resolve(ok({
|
||||
events: entries(page) as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
}))
|
||||
await opening
|
||||
const seqs = session.getSnapshot().nodes.map(n => n.seq)
|
||||
@@ -631,7 +631,7 @@ describe('paging', () => {
|
||||
gate.resolve(ok({
|
||||
events: entries(plainTurn(0, 0, 'a', 'b')) as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
}))
|
||||
await Promise.all([first, second])
|
||||
expect(api.callsOf('session.history')).toHaveLength(2) // open + one page, not two
|
||||
@@ -1018,7 +1018,7 @@ describe('remaining branches', () => {
|
||||
stale.resolve(ok({
|
||||
events: entries(plainTurn(0, 0, '旧', '代')) as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'stale' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'stale' },
|
||||
})) // success, but its generation is gone
|
||||
await Promise.all([opening, resynced])
|
||||
expect(session.getSnapshot().nodes.map(n => n.seq)).toEqual([7, 9]) // only the fresh generation's window
|
||||
@@ -1041,7 +1041,7 @@ describe('remaining branches', () => {
|
||||
secondPull.resolve(ok({
|
||||
events: entries([...plainTurn(0, 0, 'a', 'b'), ...plainTurn(6, 1, 'c', 'd')]) as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'stale' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'stale' },
|
||||
}))
|
||||
await Promise.all([opening, resynced])
|
||||
expect(session.getSnapshot().openState).toBe('open')
|
||||
@@ -1059,7 +1059,7 @@ describe('remaining branches', () => {
|
||||
repairPull.resolve(ok({
|
||||
events: entries(plainTurn(0, 0, '旧', '页')) as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'stale' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'stale' },
|
||||
})) // repair result: stale, dropped
|
||||
await resynced
|
||||
expect(session.getSnapshot().nodes.map(n => n.seq)).toEqual([7, 9])
|
||||
@@ -1104,7 +1104,7 @@ describe('remaining branches', () => {
|
||||
{ event: ev.toolResult(7, 1, 'h1', 'done'), view: { for: 'result', view: { card: 'generic', title: '历史果' } } },
|
||||
] as never[],
|
||||
hasMore: false,
|
||||
modelTarget: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
modelSelection: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
|
||||
}))
|
||||
await session.open()
|
||||
expect(session.getSnapshot().nodes.at(-1)).toMatchObject({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* SlotsService terminal-design account (design.md §11-3 main landing):
|
||||
* built-in 'root', the three load-time throws (duplicate declaration /
|
||||
* undeclared contribution / cross-scope store handle), the renderer install
|
||||
* seam (double install / not installed / non-root key), store instance
|
||||
* undeclared contribution / cross-scope store handle), the renderer installation
|
||||
* contract (double install / not installed / non-root key), store instance
|
||||
* resolution and lifecycle on the ledger axis, and the entry-unload cascade.
|
||||
*/
|
||||
import { Context } from 'cordis'
|
||||
@@ -92,13 +92,13 @@ function captureHost(bench: Bench, children?: object): SlotRendererHost {
|
||||
return host
|
||||
}
|
||||
|
||||
/** Minimal independent Workspace list source for the renderer host seam. */
|
||||
/** Minimal independent Workspace list source for the renderer host contract. */
|
||||
function fakeWorkspaces() {
|
||||
const state = { items: [], phase: 'ready' as const }
|
||||
return { list: { getSnapshot: () => state, subscribe: () => () => undefined } }
|
||||
}
|
||||
|
||||
/** Minimal sessions face for the host seam (list observable + current provide projection). */
|
||||
/** Minimal sessions face for the host contract (list observable + current provide projection). */
|
||||
function fakeSessions() {
|
||||
const state = { ids: [], byId: {}, current: undefined as string | undefined }
|
||||
const absentInfo = { sessionId: undefined, hooks: { session: undefined }, props: {} }
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ev, plainTurn } from './event-script.ts'
|
||||
const at = (seq: number, e: Record<string, unknown>): SessionEvent =>
|
||||
({ seq, time: 1_700_000_000_000 + seq, ...e }) as unknown as SessionEvent
|
||||
|
||||
/** A `compact/summary` provenance event (log-only, no surfaceOp). */
|
||||
/** A `compact/summary` event (log-only, no surfaceOp). */
|
||||
function compactSummary(seq: number, summary: unknown = [{ type: 'text', text: '# 摘要\n\n保留事实' }]): SessionEvent {
|
||||
return at(seq, {
|
||||
type: 'compact/summary',
|
||||
@@ -188,7 +188,7 @@ describe('TranscriptAdapter', () => {
|
||||
|
||||
it('skips events core does not call surface-eligible, marker or not', () => {
|
||||
// The transcript is the append-origin surface, so log-only events (a chunk,
|
||||
// a turn boundary, a compact/* provenance record) and a future type core
|
||||
// a turn boundary, a `compact/*` record) and a future type core
|
||||
// has not admitted contribute no node.
|
||||
const adapter = new TranscriptAdapter()
|
||||
adapter.reset([
|
||||
@@ -313,7 +313,7 @@ describe('TranscriptAdapter', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
['absent provenance', undefined],
|
||||
['absent summary event', undefined],
|
||||
['text-less summary blocks', compactSummary(1, [{ type: 'image', data: 'nope' }])],
|
||||
['a whitespace-only summary', compactSummary(1, [{ type: 'text', text: ' ' }])],
|
||||
['an empty summary array', compactSummary(1, [])],
|
||||
@@ -345,7 +345,7 @@ describe('TranscriptAdapter', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves the summary null when the checkpoint records no provenance at all', () => {
|
||||
it('leaves the summary null when the checkpoint cites no source events', () => {
|
||||
const adapter = new TranscriptAdapter()
|
||||
adapter.reset([at(2, {
|
||||
type: 'user/message',
|
||||
@@ -361,7 +361,7 @@ describe('TranscriptAdapter', () => {
|
||||
}])
|
||||
})
|
||||
|
||||
it('skips a non-summary provenance seq before reaching the real one', () => {
|
||||
it('skips a cited non-summary seq before reaching the summary event', () => {
|
||||
const adapter = new TranscriptAdapter()
|
||||
adapter.reset([
|
||||
ev.user(0, '被压缩的问题'),
|
||||
@@ -372,7 +372,7 @@ describe('TranscriptAdapter', () => {
|
||||
expect(adapter.nodes().at(-1)).toMatchObject({ kind: 'compaction', summary: '第三个来源才是摘要' })
|
||||
})
|
||||
|
||||
it('resolves the summary once an older page supplies the provenance', () => {
|
||||
it('resolves the summary once an older page supplies the cited summary event', () => {
|
||||
const adapter = new TranscriptAdapter()
|
||||
const landed = checkpoint(8, 7, { start: 0, end: 0, sourceEventSeqs: [7, 0] })
|
||||
adapter.reset([landed])
|
||||
|
||||
Reference in New Issue
Block a user