feat(agent): unify send(target × wakeup), coalesce context/message into user/message

Replace send/steer/inject with one Agent.send primitive over the
(target × wakeup) matrix; followup/steer/inject become fixed-preset
alias methods on the now-abstract Agent class. Coalesce context/message
into user/message (injected context is a non-user source). Replace
agent/queued with agent/inbox/enqueue/dequeue/discard, add cancel
keepInbox, and add a FIFO-conservation invariant.
This commit is contained in:
Turtle
2026-07-23 19:15:45 +08:00
parent 7c0c516f60
commit 44fd93fd06
117 changed files with 1249 additions and 728 deletions

View File

@@ -332,7 +332,7 @@ export class PlanModeService extends Service {
const text = target
? 'The user switched this session to plan mode.'
: 'The user switched this session back to the default mode.'
session.append('context/message', {
session.append('user/message', {
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: 'plan-mode' },
}, { surfaceOp: 'append' })

View File

@@ -92,7 +92,7 @@ describe('plan mode through the agent loop', () => {
const result = findEvent(log, 'tool/result')
expect(result.data.isError).toBe(false)
expect(foldPlanMode(log)).toBe(true)
expect(log.some(event => event.type === 'context/message')).toBe(false)
expect(log.some(event => event.type === 'user/message' && event.data.source.kind === 'plugin')).toBe(false)
})
it('a user flip between turns lands at the boundary: one notice and a changed header with stable tool schemas', async () => {
@@ -115,9 +115,9 @@ describe('plan mode through the agent loop', () => {
const log = agent.session.events
expect(foldPlanMode(log)).toBe(true)
const notices = log.filter(event => event.type === 'context/message')
const notices = log.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
expect(notices).toHaveLength(1)
expect(findEvent(log, 'context/message').data.content).toEqual([
expect(notices[0]?.type === 'user/message' && notices[0].data.content).toEqual([
{ type: 'text', text: 'The user switched this session to plan mode.' },
])
// The changed request is logged as a complete snapshot.
@@ -163,7 +163,8 @@ describe('plan mode through the agent loop', () => {
expect(firstEnd?.seq).toBeLessThan(planMode.seq)
expect(planMode.seq).toBeLessThan(retryStart?.seq ?? 0)
expect(findEvent(log, 'request/header', 'last').data.header.system).toContain(PLAN_CONFIG.section)
expect(findEvent(log, 'context/message').data.content).toEqual([
const notice = log.find(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
expect(notice?.type === 'user/message' && notice.data.content).toEqual([
{ type: 'text', text: 'The user switched this session to plan mode.' },
])
})

View File

@@ -95,7 +95,7 @@ function header(session: Session): void {
function noticeTexts(session: Session): string[] {
return session.events
.filter(event => event.type === 'context/message')
.filter(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
.map(event => (event.data as { content: { type: string; text?: string }[] }).content.map(block => block.text ?? '').join(''))
}