refactor(agent): unify sourced message delivery

This commit is contained in:
_Kerman
2026-07-24 22:38:50 +08:00
parent 009d113e0e
commit 992cf894af
197 changed files with 1890 additions and 2132 deletions

View File

@@ -2781,11 +2781,10 @@ export function createTuiChat(
if (disposed) {
appendNotice(`Agent "${agent.id}" is disposed.`, 'error')
} else if (agent.status === 'running') {
const source = { kind: 'user' } as const
pendingSteering.add(agent.steer(content, { source }))
pendingSteering.add(agent.steer({ content, source: { kind: 'user' } }))
refreshStatus()
} else {
agent.followup(content, { source: { kind: 'user' } })
agent.followup({ content, source: { kind: 'user' } })
}
}
@@ -3073,7 +3072,7 @@ export function createTuiChat(
editor.addToHistory(text)
if (editor.getText() === value) editor.setText('')
if (prepared.additionalContext !== undefined) {
agent.inject(prepared.additionalContext.content, { source: prepared.additionalContext.source })
agent.inject({ content: prepared.additionalContext.content, source: prepared.additionalContext.source })
}
dispatchMessage(prepared.content)
}, (error: unknown) => {

View File

@@ -4,14 +4,13 @@ import AgentRegistry, {
AgentMessageId,
type Agent,
type AgentCancelCause,
type AliasSendOptions,
type AgentOptions,
type AgentStatus,
type SendOptions,
} from '@deepseek-ai/dsh-agent'
import type { ContentBlock, LlmModelContext, LlmModelInfo, LlmProviderInfo } from '@deepseek-ai/dsh-llm'
import CommandService from '@deepseek-ai/dsh-commands'
import SessionStore, { SessionId, type Session, type SessionHeader } from '@deepseek-ai/dsh-session'
import SessionStore, { SessionId, type Session, type SessionHeader, type UserMessageData } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
@@ -21,12 +20,12 @@ import { TestSessionQueryService } from './session-query.ts'
interface FakeAgent extends Agent {
status: AgentStatus
sent: ContentBlock[][]
sentOptions: (SendOptions | AliasSendOptions | undefined)[]
sentOptions: (SendOptions | undefined)[]
steered: ContentBlock[][]
steeredIds: AgentMessageId[]
steeredOptions: (AliasSendOptions | undefined)[]
steeredOptions: UserMessageData[]
injected: ContentBlock[][]
injectedOptions: (AliasSendOptions | undefined)[]
injectedOptions: UserMessageData[]
cancelled: AgentCancelCause[]
}
@@ -163,10 +162,10 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
const sent: ContentBlock[][] = []
const steered: ContentBlock[][] = []
const steeredIds: AgentMessageId[] = []
const sentOptions: (SendOptions | AliasSendOptions | undefined)[] = []
const steeredOptions: (AliasSendOptions | undefined)[] = []
const sentOptions: (SendOptions | undefined)[] = []
const steeredOptions: UserMessageData[] = []
const injected: ContentBlock[][] = []
const injectedOptions: (AliasSendOptions | undefined)[] = []
const injectedOptions: UserMessageData[] = []
const cancelled: AgentCancelCause[] = []
const agent: FakeAgent = {
id: sessionId,
@@ -182,26 +181,26 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
injected,
injectedOptions,
cancelled,
send(content, options) {
sent.push(content)
send(input, options) {
sent.push(input.content)
sentOptions.push(options)
return AgentMessageId('stub')
},
followup(content, options) {
sent.push(content)
sentOptions.push(options)
followup(input) {
sent.push(input.content)
sentOptions.push(undefined)
return AgentMessageId('stub')
},
steer(content, options) {
steered.push(content)
steeredOptions.push(options)
steer(input) {
steered.push(input.content)
steeredOptions.push(input)
const id = AgentMessageId(`steering-${steeredIds.length + 1}`)
steeredIds.push(id)
return id
},
inject(content, options) {
injected.push(content)
injectedOptions.push(options)
inject(input) {
injected.push(input.content)
injectedOptions.push(input)
return AgentMessageId('stub')
},
cancel(cause) {

View File

@@ -11,18 +11,18 @@ buffer
2| " mock • target-session"
style 1-23 dim
3| <blank>
4| "▌ "
4| " Referenced sessions · Source session (source-session) "
style 1-53 dim
5| <blank>
6| "▌ "
style 0-0 fg=bright-blue
5| "▌ You "
7| "▌ You "
style 0-0 fg=bright-blue
style 2-4 fg=bright-blue bold
6| "▌ Use @Source session "
8| "▌ Use @Source session "
style 0-0 fg=bright-blue
7| "▌ "
9| "▌ "
style 0-0 fg=bright-blue
8| <blank>
9| " Referenced sessions · Source session (source-session) "
style 1-53 dim
10| <blank>
11| " Assistant "
style 1-9 fg=bright-magenta bold

View File

@@ -1,7 +1,7 @@
terminal 100x34 buffer=normal length=38 base=4 viewport=4
terminal 100x34 buffer=normal length=36 base=2 viewport=2
lifecycle started=1 stopped=0 progress=inactive
title "Unsafe terminal title \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m"
cursor hidden column=100 viewportRow=33 bufferRow=37
cursor hidden column=100 viewportRow=33 bufferRow=35
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-blue bold
@@ -55,23 +55,20 @@ buffer
24| " Unsafe context \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-60 fg=bright-black
25| <blank>
26| " Prompt blocked: Unsafe policy \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-75 fg=yellow
27| <blank>
28| " Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
26| " Unsafe turn error \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 1-63 fg=red
29| <blank>
30| " "
31| " Question 1/1 (1 unanswered) · Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
27| <blank>
28| " "
29| " Question 1/1 (1 unanswered) · Unsafe header \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
style 2-90 fg=bright-black
32| " Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
33| " "
34| " › 1. Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m Unsafe detail \\x1b]2;snapshot-c "
30| " Unsafe question \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m "
31| " "
32| " › 1. Unsafe option \\x1b]2;snapshot-controlled\\x07\\x09\\x7f\\x9b31m Unsafe detail \\x1b]2;snapshot-c "
style 2-65 fg=bright-blue bold
style 67-97 fg=bright-black
35| " Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt "
33| " Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt "
style 2-64 dim
36| " "
37| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
34| " "
35| "deepseek-v4-flash /workspace/project ↑0 ↓0 0% context tools:collapsed"
style 0-43 dim
style 73-99 dim

View File

@@ -475,11 +475,6 @@ describe('TUI terminal-state snapshots', () => {
content: [{ type: 'text', text: `Unsafe context ${CONTROL_PROBE}` }],
source: { kind: 'plugin', plugin: `unsafe-${CONTROL_PROBE}` },
}, { surfaceOp: 'append' })
session.append('prompt/blocked', {
content: [{ type: 'text', text: 'blocked' }],
source: { kind: 'user' },
reason: `Unsafe policy ${CONTROL_PROBE}`,
})
session.append('step/end', { turn: 1, step: 1 })
session.append('turn/end', {
turn: 1,

View File

@@ -417,7 +417,6 @@ describe('resume command and /resume', () => {
[{ kind: 'error', step: 1, message: 'failed' }, 'error'],
[{ kind: 'disposed' }, 'disposed'],
[{ kind: 'max-tokens' }, 'max tokens'],
[{ kind: 'rejected', reason: 'policy' }, 'rejected'],
[{ kind: 'interrupted' }, 'interrupted'],
[{ kind: 'future-result' } as unknown as TurnEndReason, 'unknown result'],
] as const)('renders the last turn result %s', async (reason, label) => {
@@ -1182,7 +1181,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
// A non-plugin injected source (goal) has no `plugin` field, so its context
// card label falls back to the source kind.
result.session.append('user/message', { content: [{ type: 'text', text: 'goal context' }], source: { kind: 'goal', goalId: 'g1', revision: 1, round: 0 } as never }, { surfaceOp: 'append' })
result.session.append('prompt/blocked', { content: [{ type: 'text', text: 'blocked' }], source: { kind: 'user' }, reason: 'test policy' })
appendAssistant(result.session, [])
result.session.append('step/end', { turn: 1, step: 1 })
result.session.append('turn/end', { turn: 1, reason: { kind: 'aborted' } })
@@ -1263,7 +1261,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
expect(result.terminal.output).toContain('Steering')
expect(result.terminal.output).toContain('user context')
expect(result.terminal.output).toContain('Context · goal') // goal-sourced injected context labels by kind
expect(result.terminal.output).toContain('Prompt blocked')
expect(result.terminal.output).toContain('Turn cancelled')
expect(result.terminal.progress).toContain(true)
@@ -2610,12 +2607,10 @@ describe('pi-tui chat lifecycle and transcript', () => {
events.session.append('turn/start', { turn: 4, trigger: { kind: 'message', source: { kind: 'user' } } })
events.session.append('turn/end', { turn: 4, reason: { kind: 'max-tokens' } })
events.session.append('turn/start', { turn: 5, trigger: { kind: 'message', source: { kind: 'user' } } })
events.session.append('turn/end', { turn: 5, reason: { kind: 'rejected', reason: 'policy' } })
events.session.append('turn/end', { turn: 5, reason: { kind: 'interrupted' } })
events.session.append('turn/start', { turn: 6, trigger: { kind: 'message', source: { kind: 'user' } } })
events.session.append('turn/end', { turn: 6, reason: { kind: 'interrupted' } })
events.session.append('turn/start', { turn: 7, trigger: { kind: 'message', source: { kind: 'user' } } })
events.session.append('turn/end', {
turn: 7,
turn: 6,
reason: { kind: 'error', step: 1, failure: { message: 'structured provider failure', code: 'SERVER' } },
})
agentEvents(events.ctx, events.agent).emit('agent/disposed')
@@ -2625,7 +2620,6 @@ describe('pi-tui chat lifecycle and transcript', () => {
expect(events.terminal.output).toContain('Turn cancelled')
expect(events.terminal.output).toContain('structured provider failure')
expect(events.terminal.output).toContain('output-token limit')
expect(events.terminal.output).toContain('Turn rejected')
expect(events.terminal.output).toContain('previous process ended')
expect(events.terminal.output).toContain('was disposed')
await dispose(events)