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:
@@ -64,7 +64,6 @@ function precedingMessageTime(agent: Agent): number | undefined {
|
||||
case 'user/message':
|
||||
case 'assistant/message':
|
||||
case 'tool/result':
|
||||
case 'context/message':
|
||||
case 'steering/message':
|
||||
return event.time
|
||||
default:
|
||||
@@ -79,7 +78,7 @@ function precedingMessageTime(agent: Agent): number | undefined {
|
||||
function precedingStepContextTime(agent: Agent, turn: number): number | undefined {
|
||||
for (const event of [...agent.session.events].reverse()) {
|
||||
if (event.type === 'turn/start' && event.data.turn === turn) return undefined
|
||||
if (event.type === 'context/message'
|
||||
if (event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === name) {
|
||||
return event.time
|
||||
@@ -91,7 +90,7 @@ function precedingStepContextTime(agent: Agent, turn: number): number | undefine
|
||||
/** Find this plugin's latest durable injection, including a shadowed surface event. */
|
||||
function latestInjectionTime(agent: Agent): number | undefined {
|
||||
for (const event of [...agent.session.events].reverse()) {
|
||||
if (event.type === 'context/message'
|
||||
if (event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === name) {
|
||||
return event.time
|
||||
|
||||
@@ -48,7 +48,7 @@ function preparationPosition(history: readonly SessionEvent[], fail: InvariantFa
|
||||
/** Validate one plugin-attributed time reading against its session position and timestamp. */
|
||||
function validateReading(
|
||||
history: readonly SessionEvent[],
|
||||
event: SessionEvent<'context/message'>,
|
||||
event: SessionEvent<'user/message'>,
|
||||
fail: InvariantFailure,
|
||||
): void {
|
||||
const [block] = event.data.content
|
||||
@@ -84,7 +84,7 @@ function validateReading(
|
||||
/** Validate all package-owned readings already present in one session. */
|
||||
function validateSession(session: Session, fail: InvariantFailure): void {
|
||||
for (const [index, event] of session.events.entries()) {
|
||||
if (event.type !== 'context/message'
|
||||
if (event.type !== 'user/message'
|
||||
|| event.data.source.kind !== 'plugin'
|
||||
|| event.data.source.plugin !== SOURCE_NAME) continue
|
||||
validateReading(session.events.slice(0, index), event, fail)
|
||||
@@ -97,7 +97,7 @@ const install: InvariantInstaller = Object.assign((ctx: Context, fail: Invariant
|
||||
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
||||
if (eventName !== 'session/event') return
|
||||
const [session, event] = args as [Session, SessionEvent]
|
||||
if (event.type !== 'context/message'
|
||||
if (event.type !== 'user/message'
|
||||
|| event.data.source.kind !== 'plugin'
|
||||
|| event.data.source.plugin !== SOURCE_NAME) return
|
||||
validateReading(session.events, event, fail)
|
||||
|
||||
@@ -17,7 +17,7 @@ async function setup(): Promise<Context> {
|
||||
|
||||
function event(text: string, time = SECOND + 456, content?: unknown[]): SessionEvent {
|
||||
return {
|
||||
type: 'context/message',
|
||||
type: 'user/message',
|
||||
seq: 0,
|
||||
time,
|
||||
data: {
|
||||
@@ -56,7 +56,7 @@ function preparing(turn: number, step: number): Session {
|
||||
}
|
||||
|
||||
function appendReading(session: Session, text: string): void {
|
||||
session.append('context/message', {
|
||||
session.append('user/message', {
|
||||
content: [{ type: 'text', text }],
|
||||
source: { kind: 'plugin', plugin: 'time-context' },
|
||||
}, { surfaceOp: 'append' })
|
||||
@@ -162,7 +162,7 @@ describe('time-context invariants', () => {
|
||||
|
||||
it('ignores context messages owned by another package', async () => {
|
||||
const ctx = await setup()
|
||||
const other = event('unrelated') as SessionEvent<'context/message'>
|
||||
const other = event('unrelated') as SessionEvent<'user/message'>
|
||||
other.data.source = { kind: 'plugin', plugin: 'other' }
|
||||
expect(() => { ctx.emit('session/event', preparing(1, 1), other) }).not.toThrow()
|
||||
other.data.source = { kind: 'user' }
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('time-context through a real headless cordis.yml', () => {
|
||||
expect(stderr).not.toContain('UNHANDLED')
|
||||
expect(events.filter(event => event.type === 'turn/end')).toHaveLength(2)
|
||||
|
||||
const contexts = events.filter(event => event.type === 'context/message')
|
||||
const contexts = events.filter(event => event.type === 'user/message')
|
||||
const starts = events.filter(event => event.type === 'step/start')
|
||||
expect(contexts).toHaveLength(2)
|
||||
expect(starts).toHaveLength(2)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Context } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
@@ -43,9 +43,10 @@ function sessionAgent(session: Session, id = 'agent'): Agent {
|
||||
status: 'running',
|
||||
ctx: new Context(),
|
||||
send() {},
|
||||
followup() {},
|
||||
steer() {},
|
||||
inject(content, options) {
|
||||
session.append('context/message', {
|
||||
session.append('user/message', {
|
||||
content,
|
||||
source: options?.source ?? { kind: 'user' },
|
||||
}, { surfaceOp: 'append' })
|
||||
@@ -66,7 +67,7 @@ function openMessageTurn(session: Session, turn: number): void {
|
||||
function contextTexts(session: Session): string[] {
|
||||
const texts: string[] = []
|
||||
for (const event of session.events) {
|
||||
if (event.type === 'context/message'
|
||||
if (event.type === 'user/message'
|
||||
&& event.data.source.kind === 'plugin'
|
||||
&& event.data.source.plugin === 'time-context') {
|
||||
texts.push(event.data.content.find(block => block.type === 'text')?.text ?? '')
|
||||
@@ -151,8 +152,8 @@ describe('durable step context', () => {
|
||||
+ 'Elapsed since the preceding model-visible message: 1d 1h 1m 1s.',
|
||||
])
|
||||
const event = session.events.at(-1)
|
||||
expect(event?.type).toBe('context/message')
|
||||
if (event?.type !== 'context/message') throw new Error('missing time context')
|
||||
expect(event?.type).toBe('user/message')
|
||||
if (event?.type !== 'user/message') throw new Error('missing time context')
|
||||
expect(event.data.source).toEqual({ kind: 'plugin', plugin: 'time-context' })
|
||||
expect(event.surfaceOp).toBe('append')
|
||||
})
|
||||
@@ -230,10 +231,10 @@ describe('durable step context', () => {
|
||||
const original = new Session(SessionId('seed-source'))
|
||||
openMessageTurn(original, 1)
|
||||
await fire(ctx, sessionAgent(original), 1, 1)
|
||||
const user = original.events.find(event => event.type === 'user/message')
|
||||
const reading = original.events.find(event => event.type === 'context/message')
|
||||
const user = original.events.find(event => event.type === 'user/message' && event.data.source.kind === 'user')
|
||||
const reading = original.events.find(event => event.type === 'user/message' && event.data.source.kind === 'plugin')
|
||||
if (user === undefined || reading === undefined) throw new Error('missing source surface events')
|
||||
original.append('context/message', {
|
||||
original.append('user/message', {
|
||||
content: [{ type: 'text', text: 'compacted history' }],
|
||||
source: { kind: 'plugin', plugin: 'compact-basic' },
|
||||
}, {
|
||||
@@ -292,7 +293,7 @@ describe('durable step context', () => {
|
||||
openMessageTurn(session, 1)
|
||||
let ordinarySawContext = false
|
||||
ctx.on('agent/pre-step', (subject) => {
|
||||
ordinarySawContext = subject.session.events.some(event => event.type === 'context/message')
|
||||
ordinarySawContext = subject.session.events.some(event => event.type === 'user/message')
|
||||
})
|
||||
|
||||
await fire(ctx, agent, 1, 1)
|
||||
@@ -401,7 +402,8 @@ describe('real agent-loop request history', () => {
|
||||
await agent.whenIdle()
|
||||
|
||||
expect(adapter.requests).toHaveLength(2)
|
||||
const contexts = agent.session.events.filter(event => event.type === 'context/message')
|
||||
const contexts = agent.session.events.filter(
|
||||
(event): event is SessionEvent<'user/message'> => event.type === 'user/message' && event.data.source.kind === 'plugin')
|
||||
const starts = agent.session.events.filter(event => event.type === 'step/start')
|
||||
expect(contexts).toHaveLength(adapter.requests.length)
|
||||
expect(starts).toHaveLength(adapter.requests.length)
|
||||
|
||||
Reference in New Issue
Block a user