refactor(agent): remove message metadata channel
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
|
||||
Preparation calls `ctx.sessionQuery.readSurface()` once per distinct source and never rereads it after enqueue. It projects only direct-user `user/message`, direct-user `steering/message`, assistant text, and `user/message` checkpoints carrying the canonical `dsh-compact` source marker from the folded current surface. For a source prompt that already contains baked prefix context, projection reads only its model-hidden display content, preventing recursive snapshot propagation. Shadowed pre-compaction events, tools, reasoning, context, plugin-generated user messages other than marked compact checkpoints, and unfinished assistant chunks are excluded. A compacted source therefore contributes its latest checkpoint plus retained later conversation, not restored shadowed text.
|
||||
|
||||
The context source is `{ kind: 'plugin', plugin: 'session-reference' }` with `placement: 'prompt-prefix'`. Its metadata records version `1`, source ids and labels, capture seqs, compact presence, retained/omitted message counts, omitted UTF-8 bytes, and truncation state. AgentLoop writes the snapshot, `## My request:` delimiter, and effective prompt into one `user/message` or `steering/message`; the same event's model-hidden envelope retains the direct prompt and metadata for TUI/ACP replay. Later source mutation, compaction, or deletion cannot change target replay.
|
||||
The context uses a typed `{ kind: 'session-reference', ... }` source with `placement: 'prompt-prefix'`. That source records version `1`, source ids and labels, capture seqs, compact presence, retained/omitted message counts, omitted UTF-8 bytes, and truncation state. AgentLoop writes the snapshot, `## My request:` delimiter, and effective prompt into one `user/message` or `steering/message`; the same event's model-hidden envelope retains the direct prompt and source for TUI/ACP replay. Later source mutation, compaction, or deletion cannot change target replay.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Context, Service } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import type { Agent, HookContext } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { JsonValue, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionSurfaceSnapshot } from '@deepseek-ai/dsh-session-query'
|
||||
import {
|
||||
DEFAULT_CANDIDATE_LIMIT,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from './config.ts'
|
||||
import { retainReferencedSession, type ReferenceRetentionStats, type ReferencedSessionData } from './projection.ts'
|
||||
import { stringifyTagSafeJson } from './serialization.ts'
|
||||
import type { PreparedReferencedMessage, SessionReferenceCandidate, SessionReferenceInput } from './types.ts'
|
||||
import type { PreparedReferencedMessage, SessionReferenceCandidate, SessionReferenceInput, SessionReferenceSource } from './types.ts'
|
||||
|
||||
export type * from './types.ts'
|
||||
export type { Config, SessionReferenceErrorCode } from './config.ts'
|
||||
@@ -181,7 +181,7 @@ export class SessionReferenceService extends Service {
|
||||
|
||||
const rendered = this.renderSources(prepared)
|
||||
const prompt = renderPrompt(rendered.map(source => source.data))
|
||||
const meta = {
|
||||
const source: SessionReferenceSource = {
|
||||
kind: 'session-reference',
|
||||
version: 1,
|
||||
references: rendered.map((source, index) => ({
|
||||
@@ -191,12 +191,11 @@ export class SessionReferenceService extends Service {
|
||||
...source.stats,
|
||||
inputIndex: index,
|
||||
})),
|
||||
} satisfies JsonValue
|
||||
}
|
||||
const context: HookContext = {
|
||||
source: { kind: 'plugin', plugin: 'session-reference' },
|
||||
source,
|
||||
content: [{ type: 'text', text: prompt }],
|
||||
placement: 'prompt-prefix',
|
||||
meta,
|
||||
}
|
||||
return { content: acceptedContent, contexts: [context] }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,30 @@ import type { HookContext } from '@deepseek-ai/dsh-agent'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** Durable provenance for one prepared cross-session context. */
|
||||
export interface SessionReferenceSource {
|
||||
kind: 'session-reference'
|
||||
version: 1
|
||||
references: {
|
||||
sessionId: string
|
||||
label: string
|
||||
capturedThroughSeq: number | null
|
||||
compacted: boolean
|
||||
originalMessages: number
|
||||
retainedMessages: number
|
||||
omittedMessages: number
|
||||
omittedBytes: number
|
||||
truncated: boolean
|
||||
inputIndex: number
|
||||
}[]
|
||||
}
|
||||
|
||||
declare module '@deepseek-ai/dsh-llm' {
|
||||
interface MessageSourceMap {
|
||||
'session-reference': SessionReferenceSource
|
||||
}
|
||||
}
|
||||
|
||||
/** One source session selected by a host. */
|
||||
export interface SessionReferenceInput {
|
||||
/** Opaque source session identity. */
|
||||
|
||||
@@ -241,7 +241,7 @@ describe('session reference discovery and preparation', () => {
|
||||
expect(prepared.contexts).toHaveLength(1)
|
||||
const context = prepared.contexts[0]
|
||||
if (context?.content[0]?.type !== 'text') throw new Error('expected text context')
|
||||
expect(context.source).toEqual({ kind: 'plugin', plugin: 'session-reference' })
|
||||
expect(context.source).toMatchObject({ kind: 'session-reference' })
|
||||
expect(context.placement).toBe('prompt-prefix')
|
||||
expect(context.content[0].text).toContain('untrusted, read-only snapshot')
|
||||
expect(promptData(context.content[0].text)).toEqual([{
|
||||
@@ -256,7 +256,7 @@ describe('session reference discovery and preparation', () => {
|
||||
{ role: 'assistant', text: 'visible answer' },
|
||||
],
|
||||
}])
|
||||
expect(context.meta).toMatchObject({
|
||||
expect(context.source).toMatchObject({
|
||||
kind: 'session-reference',
|
||||
version: 1,
|
||||
references: [{
|
||||
@@ -354,7 +354,7 @@ describe('session reference discovery and preparation', () => {
|
||||
{ sessionId: one.id, label: 'first' },
|
||||
{ sessionId: one.id, label: 'ignored duplicate' },
|
||||
{ sessionId: two.id },
|
||||
])).resolves.toMatchObject({ contexts: [{ meta: { references: [{ label: 'first' }, { label: 'two' }] } }] })
|
||||
])).resolves.toMatchObject({ contexts: [{ source: { references: [{ label: 'first' }, { label: 'two' }] } }] })
|
||||
await expect(ctx.sessionReferences.prepare(agent, content, [{ sessionId: target.id }]))
|
||||
.rejects.toThrow(expectCode('SESSION_REFERENCE_SELF_REFERENCE'))
|
||||
await expect(ctx.sessionReferences.prepare(agent, content, [null as never]))
|
||||
@@ -432,7 +432,7 @@ describe('session reference discovery and preparation', () => {
|
||||
expect(context.content[0].text).toContain('checkpoint')
|
||||
expect(context.content[0].text).toContain('latest-')
|
||||
expect(context.content[0].text).toContain('omitted')
|
||||
expect(context.meta).toMatchObject({ references: [{ truncated: true, compacted: true }] })
|
||||
expect(context.source).toMatchObject({ references: [{ truncated: true, compacted: true }] })
|
||||
})
|
||||
|
||||
it('applies the full byte limit independently to each of three references', async () => {
|
||||
@@ -501,7 +501,6 @@ describe('session reference discovery and preparation', () => {
|
||||
displayContent: prepared.content,
|
||||
prefixContexts: [{
|
||||
source: context.source,
|
||||
...context.meta === undefined ? {} : { meta: context.meta },
|
||||
}],
|
||||
},
|
||||
}, { surfaceOp: 'append' })
|
||||
|
||||
Reference in New Issue
Block a user