refactor(agent-loop): separate injected context from turns
This commit is contained in:
@@ -54,13 +54,14 @@ import { assertNever, CallId } from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-llm-retry'
|
||||
import {
|
||||
installAgentLlmTarget,
|
||||
type AdditionalContext,
|
||||
type Agent,
|
||||
type AgentLlmTarget as LlmTarget,
|
||||
type AgentLlmTargetRef as LlmTargetRef,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-commands'
|
||||
import { encodeSessionReferenceUri } from '@deepseek-ai/dsh-session-reference'
|
||||
import { displayPromptContent, SessionId, type JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import { SessionId, type JsonValue } from '@deepseek-ai/dsh-session'
|
||||
// Side-effect type import: resolves `ctx.get('permission')` to the service.
|
||||
import type {} from '@deepseek-ai/dsh-permission'
|
||||
import type { SessionEvent, TodoItem, TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
@@ -1056,7 +1057,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
}
|
||||
const { text } = referencedPrompt
|
||||
let preparedContent: ContentBlock[] = [{ type: 'text', text }]
|
||||
let preparedContexts: NonNullable<Parameters<Agent['send']>[1]>['contexts'] = []
|
||||
let additionalContext: AdditionalContext | undefined
|
||||
if (referencedPrompt.references.length > 0) {
|
||||
const sessionReferences = ctx.get('sessionReferences')
|
||||
if (sessionReferences === undefined) {
|
||||
@@ -1072,7 +1073,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
controller.signal,
|
||||
)
|
||||
preparedContent = prepared.content
|
||||
preparedContexts = prepared.contexts
|
||||
additionalContext = prepared.additionalContext
|
||||
} catch (error: unknown) {
|
||||
if (controller.signal.aborted) return { stopReason: 'cancelled' }
|
||||
throw invalidParams(`session reference preparation failed: ${renderThrown(error)}`)
|
||||
@@ -1088,7 +1089,10 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
// produces an error stop reason).
|
||||
const stopReason = await new Promise<StopReason>((resolve, reject) => {
|
||||
rec.inflight = { resolve, reject, turn: undefined }
|
||||
rec.agent.send(preparedContent, { source: { kind: 'user' }, contexts: preparedContexts })
|
||||
if (additionalContext !== undefined) {
|
||||
rec.agent.inject(additionalContext.content, { source: additionalContext.source })
|
||||
}
|
||||
rec.agent.send(preparedContent, { source: { kind: 'user' } })
|
||||
})
|
||||
return { stopReason }
|
||||
},
|
||||
@@ -1380,7 +1384,7 @@ export function streamSessionEventUpdate(
|
||||
// Replay the user's prompt so a loaded session shows both sides of each
|
||||
// turn. Live prompt turns suppress this path to avoid duplicating what
|
||||
// the client just sent.
|
||||
for (const block of displayPromptContent(event.data)) {
|
||||
for (const block of event.data.content) {
|
||||
const content = harnessBlockToAcpContent(block)
|
||||
if (content !== undefined) {
|
||||
notify({ sessionId, update: { sessionUpdate: 'user_message_chunk', content } })
|
||||
|
||||
@@ -352,7 +352,7 @@ describe('acp bridge', () => {
|
||||
expect(harness.ctx.agents.get(SessionId(sessionId))?.session.events).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('prepares ACP session resource links and inline mentions before one atomic send', async () => {
|
||||
it('injects ACP session references before sending the direct prompt', async () => {
|
||||
harness = await makeBridgeHarness({ storageDir, withSessionReferences: true, script: [textResponse('ok')] })
|
||||
const source = harness.ctx.sessions.create(SessionId('source'), { meta: { cwd: '/source' } })
|
||||
source.append('user/message', {
|
||||
@@ -372,23 +372,21 @@ describe('acp bridge', () => {
|
||||
expect(result.stopReason).toBe('end_turn')
|
||||
|
||||
const target = harness.ctx.agents.get(SessionId(sessionId))!.session
|
||||
const user = target.events.find(event => event.type === 'user/message')
|
||||
expect(user?.type === 'user/message' && user.data.envelope).toMatchObject({
|
||||
displayContent: [{ type: 'text', text: 'use @source-inline and @source-link' }],
|
||||
prefixContexts: [{
|
||||
source: { kind: 'plugin', plugin: 'session-reference' },
|
||||
meta: {
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'source', label: 'source-inline' }],
|
||||
},
|
||||
}],
|
||||
const context = target.events.find(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'session-reference')
|
||||
expect(context?.type === 'user/message' && context.data.source).toMatchObject({
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'source', label: 'source-inline' }],
|
||||
})
|
||||
expect(target.events.some(event => event.type === 'user/message' && event.data.source.kind !== 'user')).toBe(false)
|
||||
const user = target.events.find(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'user')
|
||||
expect(user?.type === 'user/message' && user.data.content).toEqual([
|
||||
{ type: 'text', text: 'use @source-inline and @source-link' },
|
||||
])
|
||||
const request = JSON.stringify(harness.adapter.requests[0]?.messages)
|
||||
expect(request).toContain('untrusted, read-only snapshot')
|
||||
expect(request).toContain('source background')
|
||||
expect(request.indexOf('source background')).toBeLessThan(request.indexOf('## My request:'))
|
||||
expect(request.indexOf('## My request:')).toBeLessThan(request.indexOf('use @source-inline and @source-link'))
|
||||
expect(request.indexOf('source background')).toBeLessThan(request.indexOf('use @source-inline and @source-link'))
|
||||
})
|
||||
|
||||
it('rejects a failed referenced-session read before starting a turn', async () => {
|
||||
|
||||
@@ -209,22 +209,11 @@ describe('streamSessionEventUpdate', () => {
|
||||
expect(updatesFor(evt('user/message', { content: [], source: { kind: 'user' } }))).toEqual([])
|
||||
})
|
||||
|
||||
it('replays only the direct prompt from a prefixed user message', () => {
|
||||
it('does not replay injected context as a direct user prompt', () => {
|
||||
expect(updatesFor(evt('user/message', {
|
||||
content: [
|
||||
{ type: 'text', text: 'internal prefix' },
|
||||
{ type: 'text', text: '\n\n## My request:\n' },
|
||||
{ type: 'text', text: 'visible request' },
|
||||
],
|
||||
source: { kind: 'user' },
|
||||
envelope: {
|
||||
displayContent: [{ type: 'text', text: 'visible request' }],
|
||||
prefixContexts: [{ source: { kind: 'plugin', plugin: 'reference' } }],
|
||||
},
|
||||
}))).toEqual([{
|
||||
sessionUpdate: 'user_message_chunk',
|
||||
content: { type: 'text', text: 'visible request' },
|
||||
}])
|
||||
content: [{ type: 'text', text: 'internal context' }],
|
||||
source: { kind: 'plugin', plugin: 'reference' },
|
||||
}))).toEqual([])
|
||||
})
|
||||
|
||||
it('can suppress user/message chunks for live prompt turns', () => {
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
type AgentLlmTarget,
|
||||
type AgentLlmTargetRef,
|
||||
type AgentStatus,
|
||||
type HookContext,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-agent-loop'
|
||||
import type {} from '@deepseek-ai/dsh-token-meter'
|
||||
@@ -58,7 +57,6 @@ import type {
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import type {} from '@deepseek-ai/dsh-llm-retry'
|
||||
import {
|
||||
displayPromptContent,
|
||||
SessionId,
|
||||
type JsonValue,
|
||||
type Session,
|
||||
@@ -1551,13 +1549,6 @@ function sessionReferenceCard(source: unknown): string[] | undefined {
|
||||
return labels
|
||||
}
|
||||
|
||||
function promptReferenceCards(event: Extract<SessionEvent, { type: 'user/message' | 'steering/message' }>): string[][] {
|
||||
return event.data.envelope?.prefixContexts.flatMap((context) => {
|
||||
const card = sessionReferenceCard(context.source)
|
||||
return card === undefined ? [] : [card]
|
||||
}) ?? []
|
||||
}
|
||||
|
||||
function activeToolCallIds(session: Session, active: ReadonlySet<number>): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
for (const event of session.events) {
|
||||
@@ -1974,28 +1965,20 @@ export function createTuiChat(
|
||||
}
|
||||
break
|
||||
}
|
||||
const text = displayText(contentText(displayPromptContent(event.data)).trim())
|
||||
const text = displayText(contentText(event.data.content).trim())
|
||||
if (text) {
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(new UserMessageComponent(text, palette, mdTheme))
|
||||
if (options.addHistory) editor.addToHistory(text)
|
||||
}
|
||||
for (const references of promptReferenceCards(event)) {
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(new Text(palette.dim(`Referenced sessions · ${references.map(displayText).join(', ')}`), 1, 0))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'steering/message': {
|
||||
const text = displayText(contentText(displayPromptContent(event.data)).trim())
|
||||
const text = displayText(contentText(event.data.content).trim())
|
||||
if (text) {
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(new UserMessageComponent(text, palette, mdTheme, 'Steering'))
|
||||
}
|
||||
for (const references of promptReferenceCards(event)) {
|
||||
chat.addChild(new Spacer(1))
|
||||
chat.addChild(new Text(palette.dim(`Referenced sessions · ${references.map(displayText).join(', ')}`), 1, 0))
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'prompt/blocked':
|
||||
@@ -2518,19 +2501,19 @@ export function createTuiChat(
|
||||
).finally(() => { commandControllers.delete(controller) })
|
||||
}
|
||||
|
||||
const dispatchMessage = (content: ContentBlock[], contexts: HookContext[]): void => {
|
||||
const dispatchMessage = (content: ContentBlock[]): void => {
|
||||
if (disposed) {
|
||||
appendNotice(`Agent "${agent.id}" is disposed.`, 'error')
|
||||
} else if (agent.status === 'running') {
|
||||
agent.steer(content, { source: { kind: 'user' }, contexts })
|
||||
agent.steer(content, { source: { kind: 'user' } })
|
||||
} else {
|
||||
agent.send(content, { source: { kind: 'user' }, contexts })
|
||||
agent.send(content, { source: { kind: 'user' } })
|
||||
}
|
||||
}
|
||||
|
||||
/** Deliver a user turn to the agent: steer while running, send while idle, or report a disposed agent. */
|
||||
const deliver = (payload: string): void => {
|
||||
dispatchMessage([{ type: 'text', text: payload }], [])
|
||||
dispatchMessage([{ type: 'text', text: payload }])
|
||||
}
|
||||
|
||||
/** Load a manually invoked skill and deliver its rendered body as a user turn, reporting lookup outcomes as notices. */
|
||||
@@ -2671,7 +2654,7 @@ export function createTuiChat(
|
||||
if (parsed.references.length === 0) {
|
||||
editor.addToHistory(text)
|
||||
editor.setText('')
|
||||
dispatchMessage([{ type: 'text', text: parsed.text }], [])
|
||||
dispatchMessage([{ type: 'text', text: parsed.text }])
|
||||
return
|
||||
}
|
||||
const sessionReferences = ctx.get('sessionReferences')
|
||||
@@ -2692,7 +2675,10 @@ export function createTuiChat(
|
||||
if (disposed) return
|
||||
editor.addToHistory(text)
|
||||
if (editor.getText() === value) editor.setText('')
|
||||
dispatchMessage(prepared.content, prepared.contexts)
|
||||
if (prepared.additionalContext !== undefined) {
|
||||
agent.inject(prepared.additionalContext.content, { source: prepared.additionalContext.source })
|
||||
}
|
||||
dispatchMessage(prepared.content)
|
||||
}, (error: unknown) => {
|
||||
if (!disposed && !controller.signal.aborted) {
|
||||
restoreSubmittedInput()
|
||||
|
||||
@@ -22,6 +22,8 @@ interface FakeAgent extends Agent {
|
||||
sentOptions: (SendOptions | undefined)[]
|
||||
steered: ContentBlock[][]
|
||||
steeredOptions: (SendOptions | undefined)[]
|
||||
injected: ContentBlock[][]
|
||||
injectedOptions: (SendOptions | undefined)[]
|
||||
cancelled: AgentCancelCause[]
|
||||
}
|
||||
|
||||
@@ -138,6 +140,8 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
const steered: ContentBlock[][] = []
|
||||
const sentOptions: (SendOptions | undefined)[] = []
|
||||
const steeredOptions: (SendOptions | undefined)[] = []
|
||||
const injected: ContentBlock[][] = []
|
||||
const injectedOptions: (SendOptions | undefined)[] = []
|
||||
const cancelled: AgentCancelCause[] = []
|
||||
const agent: FakeAgent = {
|
||||
id: sessionId,
|
||||
@@ -149,6 +153,8 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
sentOptions,
|
||||
steered,
|
||||
steeredOptions,
|
||||
injected,
|
||||
injectedOptions,
|
||||
cancelled,
|
||||
send(content, options) {
|
||||
sent.push(content)
|
||||
@@ -165,7 +171,11 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
steeredOptions.push(options)
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
inject: () => AgentMessageId('stub'),
|
||||
inject(content, options) {
|
||||
injected.push(content)
|
||||
injectedOptions.push(options)
|
||||
return AgentMessageId('stub')
|
||||
},
|
||||
cancel(cause = { kind: 'user' }) {
|
||||
cancelled.push(cause)
|
||||
},
|
||||
|
||||
@@ -24,10 +24,10 @@ class SnapshotAdapter extends LlmAdapter {
|
||||
|
||||
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
this.requests.push(options)
|
||||
const prompt = options.messages.at(-1)
|
||||
if (prompt?.role !== 'user' || prompt.content.length !== 3
|
||||
|| prompt.content[1]?.type !== 'text' || prompt.content[1].text !== '\n\n## My request:\n') {
|
||||
throw new Error('session reference did not reach the model as one prefixed user message')
|
||||
const [context, prompt] = options.messages.slice(-2)
|
||||
if (context?.role !== 'user' || prompt?.role !== 'user'
|
||||
|| prompt.content[0]?.type !== 'text' || prompt.content[0].text !== 'Use @Source session') {
|
||||
throw new Error('session reference context did not precede the direct user message')
|
||||
}
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 0, text: 'Combined reference request accepted.' }
|
||||
@@ -113,21 +113,17 @@ describe('TUI session-reference snapshot', () => {
|
||||
expect(request).toContain('Recent retained question.')
|
||||
expect(request).not.toContain('SHADOWED OLD USER')
|
||||
expect(request).not.toContain('SHADOWED OLD ASSISTANT')
|
||||
const user = target.session.events.find(event => event.type === 'user/message')
|
||||
expect(user?.type === 'user/message' && user.data.envelope).toMatchObject({
|
||||
displayContent: [{ type: 'text', text: 'Use @Source session' }],
|
||||
prefixContexts: [{
|
||||
source: {
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'source-session', compacted: true }],
|
||||
} as never,
|
||||
}],
|
||||
const context = target.session.events.find(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'session-reference')
|
||||
expect(context?.type === 'user/message' && context.data.source).toMatchObject({
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'source-session', compacted: true }],
|
||||
})
|
||||
expect(user?.type === 'user/message' && user.data.content[1]).toEqual({
|
||||
type: 'text',
|
||||
text: '\n\n## My request:\n',
|
||||
})
|
||||
expect(target.session.events.some(event => event.type === 'user/message' && event.data.source.kind !== 'user')).toBe(false)
|
||||
const user = target.session.events.find(event =>
|
||||
event.type === 'user/message' && event.data.source.kind === 'user')
|
||||
expect(user?.type === 'user/message' && user.data.content).toEqual([
|
||||
{ type: 'text', text: 'Use @Source session' },
|
||||
])
|
||||
|
||||
const snapshot = await terminal.snapshot({ includeScrollback: true })
|
||||
if (REFRESHING) {
|
||||
|
||||
@@ -575,7 +575,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.terminal.output).not.toContain('queued')
|
||||
|
||||
const queueSteering = (text: string): void => {
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text }], source: { kind: 'user' }, steering: true, wakeup: true })
|
||||
}
|
||||
const drainSteering = (text: string): void => {
|
||||
result.session.append('steering/message', { turn: 1, content: [{ type: 'text', text }], source: { kind: 'user' } }, { surfaceOp: 'append' })
|
||||
@@ -584,7 +584,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
// A steering queue for a different agent never touches this status line.
|
||||
const other = { ...result.agent, id: SessionId('other') } as unknown as Agent
|
||||
result.terminal.output = ''
|
||||
result.ctx.emit('agent/inbox/enqueue', other, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'elsewhere' }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
result.ctx.emit('agent/inbox/enqueue', other, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'elsewhere' }], source: { kind: 'user' }, steering: true, wakeup: true })
|
||||
await tick()
|
||||
expect(result.terminal.output).not.toContain('queued')
|
||||
|
||||
@@ -597,7 +597,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
|
||||
// A non-steering queue (an idle-style send) leaves the badge untouched.
|
||||
result.terminal.output = ''
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'sent' }], source: { kind: 'user' }, contexts: [], steering: false, wakeup: true })
|
||||
result.ctx.emit('agent/inbox/enqueue', result.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'sent' }], source: { kind: 'user' }, steering: false, wakeup: true })
|
||||
drainSteering('first')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('1 queued')
|
||||
@@ -651,7 +651,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
const idle = await setup()
|
||||
// A steering queue arriving while idle has no status line to badge, so the
|
||||
// refresh is a no-op beyond requesting a render.
|
||||
idle.ctx.emit('agent/inbox/enqueue', idle.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'early' }], source: { kind: 'user' }, contexts: [], steering: true, wakeup: true })
|
||||
idle.ctx.emit('agent/inbox/enqueue', idle.agent, { id: AgentMessageId('stub'), content: [{ type: 'text', text: 'early' }], source: { kind: 'user' }, steering: true, wakeup: true })
|
||||
idle.session.append('tool/call', { turn: 1, step: 0, callId: 'pre' as never, name: 'bash', arguments: '{}' })
|
||||
await tick()
|
||||
expect(idle.terminal.output).not.toContain('Executing tools')
|
||||
@@ -1063,19 +1063,18 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.terminal.send('\r')
|
||||
await vi.waitFor(() => { expect(result.agent.sent).toHaveLength(1) })
|
||||
expect(result.agent.sent).toEqual([[{ type: 'text', text: '@Source chat' }]])
|
||||
expect(result.agent.sentOptions[0]?.contexts).toHaveLength(1)
|
||||
expect(result.agent.injected).toHaveLength(1)
|
||||
|
||||
const mention = formatSessionReferenceMention({ sessionId: sourceId, label: 'Source chat' })
|
||||
expect(result.agent.sentOptions[0]?.contexts).toMatchObject([{
|
||||
source: { kind: 'session-reference', references: [{ sessionId: 'source-session' }] },
|
||||
}])
|
||||
expect(result.agent.injectedOptions[0]?.source)
|
||||
.toMatchObject({ kind: 'session-reference', references: [{ sessionId: 'source-session' }] })
|
||||
|
||||
result.agent.status = 'running'
|
||||
result.terminal.send(`steer ${mention}`)
|
||||
result.terminal.send('\r')
|
||||
await vi.waitFor(() => { expect(result.agent.steered).toHaveLength(1) })
|
||||
expect(result.agent.steered).toEqual([[{ type: 'text', text: 'steer @Source chat' }]])
|
||||
expect(result.agent.steeredOptions[0]?.contexts).toHaveLength(1)
|
||||
expect(result.agent.injected).toHaveLength(2)
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
@@ -1115,7 +1114,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.terminal.send('\r')
|
||||
await vi.waitFor(() => { expect(result.agent.sent).toHaveLength(1) })
|
||||
expect(result.agent.sent[0]).toEqual([{ type: 'text', text: '@src/source-file.ts' }])
|
||||
expect(result.agent.sentOptions[0]?.contexts).toEqual([])
|
||||
|
||||
result.terminal.send('@do')
|
||||
await vi.waitFor(() => {
|
||||
@@ -1130,7 +1128,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
result.terminal.send('\r')
|
||||
await vi.waitFor(() => { expect(result.agent.sent).toHaveLength(2) })
|
||||
expect(result.agent.sent[1]).toEqual([{ type: 'text', text: '@"docs/design notes.md"' }])
|
||||
expect(result.agent.sentOptions[1]?.contexts).toEqual([])
|
||||
|
||||
result.terminal.send('@unsafe')
|
||||
await tick()
|
||||
@@ -1226,9 +1223,8 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.agent.sent).toEqual([[
|
||||
{ type: 'text', text: '@evil\\x1b\\x07\\x9b\\x0as' },
|
||||
]])
|
||||
expect(result.agent.sentOptions[0]?.contexts).toMatchObject([{
|
||||
meta: { references: [{ sessionId: unsafeId }] },
|
||||
}])
|
||||
expect(result.agent.injectedOptions[0]?.source)
|
||||
.toMatchObject({ references: [{ sessionId: unsafeId }] })
|
||||
await dispose(result)
|
||||
})
|
||||
|
||||
@@ -1314,52 +1310,37 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.terminal.output).toContain('keep @[')
|
||||
|
||||
result.session.append('user/message', {
|
||||
content: [
|
||||
{ type: 'text', text: 'hidden baked snapshot payload' },
|
||||
{ type: 'text', text: '\n\n## My request:\n' },
|
||||
{ type: 'text', text: 'visible referenced question' },
|
||||
],
|
||||
content: [{ type: 'text', text: 'hidden snapshot payload' }],
|
||||
source: {
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'prefixed', label: 'Prefixed source' }],
|
||||
} as never,
|
||||
}, { surfaceOp: 'append' })
|
||||
result.session.append('user/message', {
|
||||
content: [{ type: 'text', text: 'visible referenced question' }],
|
||||
source: { kind: 'user' },
|
||||
envelope: {
|
||||
displayContent: [{ type: 'text', text: 'visible referenced question' }],
|
||||
prefixContexts: [{
|
||||
source: {
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'prefixed', label: 'Prefixed source' }],
|
||||
} as never,
|
||||
}],
|
||||
},
|
||||
}, { surfaceOp: 'append' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('visible referenced question')
|
||||
expect(result.terminal.output).toContain('Referenced sessions · Prefixed source (prefixed)')
|
||||
expect(result.terminal.output).not.toContain('hidden baked snapshot payload')
|
||||
expect(result.terminal.output).not.toContain('hidden snapshot payload')
|
||||
|
||||
result.session.append('user/message', {
|
||||
content: [{ type: 'text', text: 'hidden steering context' }],
|
||||
source: {
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'steering-source', label: 'Steering source' }],
|
||||
} as never,
|
||||
}, { surfaceOp: 'append' })
|
||||
result.session.append('steering/message', {
|
||||
turn: 1,
|
||||
content: [
|
||||
{ type: 'text', text: 'hidden non-reference prefix' },
|
||||
{ type: 'text', text: '\n\n## My request:\n' },
|
||||
{ type: 'text', text: 'visible steering prompt' },
|
||||
],
|
||||
content: [{ type: 'text', text: 'visible steering prompt' }],
|
||||
source: { kind: 'user' },
|
||||
envelope: {
|
||||
displayContent: [{ type: 'text', text: 'visible steering prompt' }],
|
||||
prefixContexts: [
|
||||
{ source: { kind: 'plugin', plugin: 'other' } },
|
||||
{
|
||||
source: {
|
||||
kind: 'session-reference',
|
||||
references: [{ sessionId: 'steering-source', label: 'Steering source' }],
|
||||
} as never,
|
||||
},
|
||||
],
|
||||
},
|
||||
}, { surfaceOp: 'append' })
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('visible steering prompt')
|
||||
expect(result.terminal.output).toContain('Referenced sessions · Steering source (steering-source)')
|
||||
expect(result.terminal.output).not.toContain('hidden non-reference prefix')
|
||||
expect(result.terminal.output).not.toContain('hidden steering context')
|
||||
|
||||
result.session.append('user/message', {
|
||||
content: [{ type: 'text', text: 'secret full snapshot payload' }],
|
||||
@@ -1426,7 +1407,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
let release: (() => void) | undefined
|
||||
const prepare = vi.spyOn(result.ctx.sessionReferences, 'prepare').mockImplementation(
|
||||
(_agent, content) => new Promise((resolve) => {
|
||||
release = () => { resolve({ content, contexts: [] }) }
|
||||
release = () => { resolve({ content }) }
|
||||
}),
|
||||
)
|
||||
result.terminal.send(value)
|
||||
@@ -1474,7 +1455,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
let resolveAfterDispose: (() => void) | undefined
|
||||
const latePrepare = vi.spyOn(lateSuccess.ctx.sessionReferences, 'prepare').mockImplementation(
|
||||
(_agent, content) => new Promise((resolve) => {
|
||||
resolveAfterDispose = () => { resolve({ content, contexts: [] }) }
|
||||
resolveAfterDispose = () => { resolve({ content }) }
|
||||
}),
|
||||
)
|
||||
lateSuccess.terminal.send(value)
|
||||
|
||||
Reference in New Issue
Block a user