fix(agent): address initial review findings

This commit is contained in:
_Kerman
2026-07-30 18:54:19 +08:00
parent 934e5e957c
commit c891cb6f6f
10 changed files with 72 additions and 21 deletions

View File

@@ -1502,6 +1502,20 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
const view = viewFor(ctx, event, callId =>
openCalls.get(session.id)?.get(callId) ?? backscanArgs(session.events, callId))
queue.push(frame({ type: 'session/event', sessionId: session.id, event, ...view === undefined ? {} : { view } }))
if (event.type === 'agent/inbox/spliced' && event.data.target === 'next-turn') {
const agent = ctx.agents.get(session.id)
if (agent?.session === session) {
queue.push(frame({
type: 'session/queue',
sessionId: session.id,
items: agent.inbox.nextTurn.toSpliced(
event.data.start,
event.data.removedCount ?? 0,
...event.data.inserted,
),
}))
}
}
}),
ctx.on('session/created', (session: Session) => {
subscribeSession(queue, session)

View File

@@ -62,9 +62,9 @@ export type MuxFrame =
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
/**
* Complete next-turn queue baseline emitted when a mux stream opens. Live
* mutations arrive through durable `agent/inbox/spliced` session events.
* Pending next-step input is outside this Web queue projection.
* Complete next-turn queue snapshot emitted when a mux stream opens and
* after every live next-turn mutation. Pending next-step input is outside
* this Web queue projection.
*/
| { type: 'session/queue'; sessionId: SessionId; items: UserMessage[] }
/**

View File

@@ -20,7 +20,7 @@ import ToolRegistry from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import CommandService from '@deepseek-ai/dsh-commands'
import SkillService from '@deepseek-ai/dsh-skill'
import type { HostFrame, MuxFrame } from '../src/api/index.ts'
import type { HostFrame } from '../src/api/index.ts'
import type { RpcRequest, RpcResponse } from '../src/api/rpc.ts'
import { RpcId } from '../src/api/rpc.ts'
import { createApiProxy } from '../src/api-proxy.ts'
@@ -85,6 +85,13 @@ async function collect<F>(iterable: AsyncIterable<RpcRequest<F>>, count: number,
return frames
}
/** Read the next payload from an open stream. */
async function nextFrame<F>(iterator: AsyncIterator<RpcRequest<F>>): Promise<F> {
const result = await iterator.next()
if (result.done) throw new Error('stream ended')
return result.value.payload
}
describe('command.list', () => {
it('serves the addressed agent\'s name-sorted catalog', async () => {
const ctx = await harness()
@@ -332,24 +339,41 @@ describe('session.updateQueue', () => {
})
describe('session/queue frames', () => {
it('publishes the durable next-turn baseline without duplicating message identity', async () => {
it('publishes authoritative next-turn snapshots without duplicating message identity', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
const agent = stubAgent(ctx)
const queued = inboxMessage('m-1', 'queued prompt')
const edited = inboxMessage('m-1', 'edited prompt')
const steering = inboxMessage('m-2', 'steering prompt')
agent.inbox.splice('next-turn', 0, 0, [queued])
agent.inbox.splice('next-step', 0, 0, [steering])
const abort = new AbortController()
const frames = await collect<MuxFrame>(
api.events.mux({ rpcId: RpcId('t-mux-baseline'), payload: {} }, abort.signal), 2, abort)
const iterator = api.events.mux({
rpcId: RpcId('t-mux-baseline'),
payload: {},
}, abort.signal)[Symbol.asyncIterator]()
const frames = [
await nextFrame(iterator),
await nextFrame(iterator),
]
agent.inbox.splice('next-turn', 0, 1, [edited])
frames.push(await nextFrame(iterator), await nextFrame(iterator))
abort.abort()
await iterator.return?.()
expect(frames.filter(frame => frame.type === 'session/queue')).toEqual([
{
type: 'session/queue',
sessionId: agent.id,
items: [queued],
},
{
type: 'session/queue',
sessionId: agent.id,
items: [edited],
},
])
})
})

View File

@@ -10,7 +10,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { z } from 'zod'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -53,9 +53,8 @@ async function harness(withRegistry: boolean): Promise<{ ctx: Context; session:
await ctx.plugin(AgentRegistry)
if (withRegistry) await ctx.plugin(SessionProjectionRegistry)
const session = ctx.sessions.create()
// history resolves the agent first; a live structural stub is enough (only
// .session is read on this path).
ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent)
// The gateway reads both the session and durable inbox baseline.
ctx.agents.register({ id: session.id, session, inbox: new Inbox(session), status: 'idle', ctx } as Agent)
return { ctx, session }
}