refactor: identify and freeze messages at creation
This commit is contained in:
@@ -5,9 +5,9 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
|
||||
import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent, type UserMessageData } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { agentEvents, AgentMessageId, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import LlmService, { createUserMessage, CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent, type UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
|
||||
import type {
|
||||
@@ -178,13 +178,12 @@ function stubAgent(cwd?: string, seed: SessionEvent[] = []): Agent {
|
||||
session,
|
||||
status: 'idle',
|
||||
acceptsNextStep: false,
|
||||
followup: () => AgentMessageId('stub'),
|
||||
steer: () => AgentMessageId('stub'),
|
||||
followup: () => {},
|
||||
steer: () => {},
|
||||
inject(input) {
|
||||
session.append('user/message', input, { surfaceOp: 'append' })
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
send: () => AgentMessageId('stub'),
|
||||
send: () => {},
|
||||
cancel() {},
|
||||
whenIdle: () => Promise.resolve(),
|
||||
}
|
||||
@@ -201,7 +200,7 @@ function blocksText(blocks: { type: string; text?: string }[] | undefined): stri
|
||||
return blocks?.map(block => block.type === 'text' ? block.text ?? '' : '').join('\n') ?? ''
|
||||
}
|
||||
|
||||
function workspaceContextOf(result: { additionalContexts?: UserMessageData[] }): UserMessageData | undefined {
|
||||
function workspaceContextOf(result: { additionalContexts?: UserMessage[] }): UserMessage | undefined {
|
||||
return result.additionalContexts?.find(context =>
|
||||
context.source.kind === 'workspace-instructions')
|
||||
}
|
||||
@@ -213,23 +212,20 @@ function baselineEvents(agent: Agent): SessionEvent[] {
|
||||
&& event.data.source.baseline === true)
|
||||
}
|
||||
|
||||
function workspaceChangeContext(scope: string, digest: string): UserMessageData {
|
||||
return {
|
||||
function workspaceChangeContext(scope: string, digest: string): UserMessage {
|
||||
return createUserMessage({
|
||||
content: [{ type: 'text', text: `instructions for ${scope}` }],
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
changes: [{ action: 'set', scope, path: `${scope}/AGENTS.md`, digest }],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function appendAdditionalContexts(agent: Agent, result: { additionalContexts?: UserMessageData[] }): number | undefined {
|
||||
function appendAdditionalContexts(agent: Agent, result: { additionalContexts?: UserMessage[] }): number | undefined {
|
||||
let lastSeq: number | undefined
|
||||
for (const context of result.additionalContexts ?? []) {
|
||||
lastSeq = agent.session.append('user/message', {
|
||||
content: context.content,
|
||||
source: context.source,
|
||||
}, { surfaceOp: 'append' }).seq
|
||||
lastSeq = agent.session.append('user/message', context, { surfaceOp: 'append' }).seq
|
||||
}
|
||||
return lastSeq
|
||||
}
|
||||
@@ -953,6 +949,7 @@ describe('workspace context request injection', () => {
|
||||
expect(baselineEvents(agent)[0]).toMatchObject({
|
||||
type: 'user/message',
|
||||
data: {
|
||||
role: 'user',
|
||||
source: {
|
||||
kind: 'workspace-instructions',
|
||||
baseline: true,
|
||||
@@ -960,6 +957,8 @@ describe('workspace context request injection', () => {
|
||||
},
|
||||
},
|
||||
})
|
||||
const baseline = baselineEvents(agent)[0]
|
||||
expect(baseline?.type === 'user/message' && Array.isArray(baseline.data.content)).toBe(true)
|
||||
expect(composedPrefixes.get(agent)).toHaveLength(1)
|
||||
expect(derivedText(agent)).toContain('<system-reminder>')
|
||||
expect(derivedText(agent)).toContain('Instructions from: AGENTS.md')
|
||||
@@ -1045,10 +1044,10 @@ describe('workspace context request injection', () => {
|
||||
const baseline = baselineEvents(agent)[0]
|
||||
expect(baseline).toBeDefined()
|
||||
|
||||
agent.session.append('user/message', {
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'compacted summary' }],
|
||||
source: { kind: 'plugin', plugin: 'compact' },
|
||||
}, {
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: baseline!.seq, end: baseline!.seq },
|
||||
sourceEventSeqs: [baseline!.seq],
|
||||
})
|
||||
@@ -1135,7 +1134,7 @@ describe('workspace context request injection', () => {
|
||||
const ctx = new Context()
|
||||
await mountWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
|
||||
ctx.on('agent/step', (agent) => {
|
||||
agent.inject({ content: [{ type: 'text', text: '<system-reminder>Available skills</system-reminder>' }], source: { kind: 'plugin', plugin: 'test-skills' } })
|
||||
agent.inject(createUserMessage({ content: [{ type: 'text', text: '<system-reminder>Available skills</system-reminder>' }], source: { kind: 'plugin', plugin: 'test-skills' } }))
|
||||
})
|
||||
|
||||
const prefix = await composeBaselinePrefix(ctx, stubAgent(root))
|
||||
@@ -1831,13 +1830,13 @@ describe('dynamic nested workspace context injection', () => {
|
||||
},
|
||||
}))
|
||||
|
||||
agent.followup({ content: [{ type: 'text', text: 'read and abort' }], source: { kind: 'user' } })
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'read and abort' }], source: { kind: 'user' } }))
|
||||
await agent.whenIdle()
|
||||
expect(agent.session.events.filter(event =>
|
||||
event.type === 'user/message' && event.data.source.kind !== 'user',
|
||||
)).toHaveLength(0)
|
||||
|
||||
agent.followup({ content: [{ type: 'text', text: 'retry the read' }], source: { kind: 'user' } })
|
||||
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'retry the read' }], source: { kind: 'user' } }))
|
||||
await agent.whenIdle()
|
||||
|
||||
const contexts = agent.session.events.filter(event => event.type === 'user/message' && event.data.source.kind !== 'user')
|
||||
@@ -2689,10 +2688,10 @@ describe('dynamic nested workspace context injection', () => {
|
||||
agent,
|
||||
})
|
||||
|
||||
agent.session.append('user/message', {
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'compacted summary' }],
|
||||
source: { kind: 'plugin', plugin: 'compact' },
|
||||
}, {
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: contextSeq, end: contextSeq },
|
||||
sourceEventSeqs: [contextSeq],
|
||||
})
|
||||
@@ -2736,10 +2735,10 @@ describe('dynamic nested workspace context injection', () => {
|
||||
arguments: { file_path: 'file.txt' },
|
||||
agent,
|
||||
})
|
||||
agent.session.append('user/message', {
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'compacted summary' }],
|
||||
source: { kind: 'plugin', plugin: 'compact' },
|
||||
}, {
|
||||
}), {
|
||||
surfaceOp: { op: 'replace', start: baseline!.seq, end: baseline!.seq },
|
||||
sourceEventSeqs: [baseline!.seq],
|
||||
})
|
||||
@@ -2859,7 +2858,7 @@ describe('dynamic nested workspace context injection', () => {
|
||||
const ctx = new Context()
|
||||
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
|
||||
const agent = stubAgent(root)
|
||||
agent.session.append('user/message', {
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [
|
||||
{ type: 'reasoning', text: 'Additional instructions from: pkg/AGENTS.md' },
|
||||
{ type: 'text', text: 'Updated instructions from: pkg/AGENTS.md' },
|
||||
@@ -2873,15 +2872,15 @@ describe('dynamic nested workspace context injection', () => {
|
||||
{ action: 'set', scope: 'pkg', path: join('pkg', 'AGENTS.md'), digest: 42 },
|
||||
],
|
||||
} as never,
|
||||
}, { surfaceOp: 'append' })
|
||||
agent.session.append('user/message', {
|
||||
}), { surfaceOp: 'append' })
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'stale metadata version' }],
|
||||
source: { kind: 'workspace-instructions', changes: 'invalid' } as never,
|
||||
}, { surfaceOp: 'append' })
|
||||
agent.session.append('user/message', {
|
||||
}), { surfaceOp: 'append' })
|
||||
agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'foreign plugin context' }],
|
||||
source: { kind: 'plugin', plugin: 'other' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
signal: testToolSignal,
|
||||
@@ -3025,10 +3024,10 @@ describe('dynamic nested workspace context injection', () => {
|
||||
lines: [{ number: 1, text: 'downstream replacement' }],
|
||||
totalLines: 1,
|
||||
},
|
||||
additionalContexts: [{
|
||||
additionalContexts: [createUserMessage({
|
||||
content: [{ type: 'text' as const, text: 'downstream context' }],
|
||||
source: { kind: 'plugin' as const, plugin: 'downstream' },
|
||||
}],
|
||||
})],
|
||||
}))
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
@@ -3228,7 +3227,9 @@ describe('dynamic nested workspace context injection', () => {
|
||||
ctx.emit('tools/result', stubToolExecution({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('contextless-child'), name: 'read', arguments: {}, agent, parent,
|
||||
}), { ...plainResult, additionalContexts: [{ content: [], source: { kind: 'plugin', plugin: 'workspace-context' } }] })
|
||||
}), { ...plainResult, additionalContexts: [createUserMessage({
|
||||
content: [], source: { kind: 'plugin', plugin: 'workspace-context' },
|
||||
})] })
|
||||
ctx.emit('tools/result', stubToolExecution({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('first-child'), name: 'read', arguments: {}, agent, parent,
|
||||
@@ -3401,25 +3402,25 @@ describe('workspace context pending state', () => {
|
||||
path: join('pkg', 'AGENTS.md'), version: FsVersion('v1'), digest: 'one', trimmedDigest: 'one',
|
||||
}]]))
|
||||
|
||||
const unrelated = agent.session.append('user/message', {
|
||||
const unrelated = agent.session.append('user/message', createUserMessage({
|
||||
content: [], source: { kind: 'plugin', plugin: 'other' },
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
observeInstructionSessionEvent(agent.session, unrelated, pending, versions)
|
||||
expect(pending.get(agent.session)?.has('pkg')).toBe(true)
|
||||
|
||||
const otherContext = workspaceChangeContext('other', 'other')
|
||||
const otherWorkspaceEvent = agent.session.append('user/message', {
|
||||
const otherWorkspaceEvent = agent.session.append('user/message', createUserMessage({
|
||||
content: otherContext.content,
|
||||
source: otherContext.source,
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
observeInstructionSessionEvent(agent.session, otherWorkspaceEvent, pending, versions)
|
||||
expect(pending.get(agent.session)?.has('pkg')).toBe(true)
|
||||
|
||||
const context = workspaceChangeContext('pkg', 'one')
|
||||
const confirmed = agent.session.append('user/message', {
|
||||
const confirmed = agent.session.append('user/message', createUserMessage({
|
||||
content: context.content,
|
||||
source: context.source,
|
||||
}, { surfaceOp: 'append' })
|
||||
}), { surfaceOp: 'append' })
|
||||
observeInstructionSessionEvent(agent.session, confirmed, pending, versions)
|
||||
|
||||
expect(pending.has(agent.session)).toBe(false)
|
||||
@@ -3473,15 +3474,15 @@ describe('workspace context pending state', () => {
|
||||
rollbackPendingInstructionChanges(agent, [{
|
||||
action: 'set', scope: 'missing', path: 'missing/AGENTS.md', digest: 'none',
|
||||
}], pending)
|
||||
expect(commitPendingInstructionContexts(agent, [{
|
||||
expect(commitPendingInstructionContexts(agent, [createUserMessage({
|
||||
content: [], source: { kind: 'plugin', plugin: 'workspace-context' },
|
||||
}], pending)).toEqual([])
|
||||
})], pending)).toEqual([])
|
||||
// A workspace-instructions source whose change list filters to nothing
|
||||
// must not mint per-session pending state.
|
||||
expect(commitPendingInstructionContexts(agent, [{
|
||||
expect(commitPendingInstructionContexts(agent, [createUserMessage({
|
||||
content: [],
|
||||
source: { kind: 'workspace-instructions', changes: [] },
|
||||
}], pending)).toEqual([])
|
||||
})], pending)).toEqual([])
|
||||
expect(pending.has(agent.session)).toBe(false)
|
||||
|
||||
const committed = commitPendingInstructionContexts(agent, [
|
||||
|
||||
Reference in New Issue
Block a user