mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
refactor(ui-plan): read plans through the events page
A plan is a logged tool-call argument: read one page at the current log end and page backwards from its cut instead of opening a follow snapshot.
This commit is contained in:
@@ -11,7 +11,6 @@ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client'
|
||||
export function planFailureLine(t: TranslateNS<'plan'>, failure: RemoteFailure): string {
|
||||
switch (failure.code) {
|
||||
case 'plan/invalid-address': return t('preview.invalidAddress')
|
||||
case 'plan/unavailable': return t('preview.historyUnavailable')
|
||||
case 'plan/not-found': return t('preview.notFound')
|
||||
// Transport failures, including plan/read-failed, retain the carrier's diagnostic.
|
||||
default: return failure.message
|
||||
|
||||
@@ -12,7 +12,6 @@ export const zh = {
|
||||
'preview.loading': '正在读取计划…',
|
||||
'preview.failed': '无法读取计划',
|
||||
'preview.invalidAddress': '计划地址无效',
|
||||
'preview.historyUnavailable': '无法读取会话历史',
|
||||
'preview.notFound': '未找到这份计划',
|
||||
'preview.unavailable': '计划预览不可用',
|
||||
'preview.expired': '临时计划预览已失效,请从仍在等待审批的卡片重新打开。',
|
||||
@@ -36,7 +35,6 @@ export const en = {
|
||||
'preview.loading': 'Loading plan…',
|
||||
'preview.failed': 'Could not load plan',
|
||||
'preview.invalidAddress': 'Invalid plan address',
|
||||
'preview.historyUnavailable': 'Session history is unavailable',
|
||||
'preview.notFound': 'This plan was not found',
|
||||
'preview.unavailable': 'Plan preview is unavailable',
|
||||
'preview.expired': 'This temporary plan preview has expired. Reopen it from the pending review card.',
|
||||
@@ -57,7 +55,6 @@ export const ja = {
|
||||
'preview.loading': '計画を読み込み中…',
|
||||
'preview.failed': '計画を読み込めませんでした',
|
||||
'preview.invalidAddress': '計画のアドレスが無効です',
|
||||
'preview.historyUnavailable': 'セッション履歴を利用できません',
|
||||
'preview.notFound': 'この計画は見つかりませんでした',
|
||||
'preview.unavailable': '計画のプレビューを利用できません',
|
||||
'preview.expired': 'この一時的な計画プレビューは期限切れです。保留中のレビューカードから再度開いてください。',
|
||||
@@ -78,7 +75,6 @@ export const es = {
|
||||
'preview.loading': 'Cargando plan…',
|
||||
'preview.failed': 'No se pudo cargar el plan',
|
||||
'preview.invalidAddress': 'Dirección del plan no válida',
|
||||
'preview.historyUnavailable': 'El historial de sesiones no está disponible',
|
||||
'preview.notFound': 'Este plan no fue encontrado.',
|
||||
'preview.unavailable': 'La vista previa del plan no está disponible',
|
||||
'preview.expired': 'Esta vista previa del plan temporal ha caducado. Vuelva a abrirlo desde la tarjeta de revisión pendiente.',
|
||||
@@ -99,7 +95,6 @@ export const fr = {
|
||||
'preview.loading': 'Plan de chargement…',
|
||||
'preview.failed': 'Impossible de charger le plan',
|
||||
'preview.invalidAddress': 'Adresse du forfait invalide',
|
||||
'preview.historyUnavailable': "L'historique des sessions n'est pas disponible",
|
||||
'preview.notFound': "Ce plan n'a pas été trouvé",
|
||||
'preview.unavailable': "L'aperçu du plan n'est pas disponible",
|
||||
'preview.expired': 'Cet aperçu du plan temporaire a expiré. Rouvrez-le à partir de la carte de révision en attente.',
|
||||
@@ -120,7 +115,6 @@ export const de = {
|
||||
'preview.loading': 'Ladeplan…',
|
||||
'preview.failed': 'Plan konnte nicht geladen werden',
|
||||
'preview.invalidAddress': 'Ungültige Planadresse',
|
||||
'preview.historyUnavailable': 'Der Sitzungsverlauf ist nicht verfügbar',
|
||||
'preview.notFound': 'Dieser Plan wurde nicht gefunden',
|
||||
'preview.unavailable': 'Die Planvorschau ist nicht verfügbar',
|
||||
'preview.expired': 'Diese temporäre Planvorschau ist abgelaufen. Öffnen Sie es erneut über die ausstehende Überprüfungskarte.',
|
||||
@@ -141,7 +135,6 @@ export const ru = {
|
||||
'preview.loading': 'План загрузки…',
|
||||
'preview.failed': 'Не удалось загрузить план',
|
||||
'preview.invalidAddress': 'Неверный адрес плана',
|
||||
'preview.historyUnavailable': 'История сеансов недоступна',
|
||||
'preview.notFound': 'Этот план не найден',
|
||||
'preview.unavailable': 'Предварительный просмотр плана недоступен.',
|
||||
'preview.expired': 'Срок действия этой временной предварительной версии плана истек. Откройте его снова из карточки ожидающего рассмотрения.',
|
||||
@@ -162,7 +155,6 @@ export const pt = {
|
||||
'preview.loading': 'Carregando plano…',
|
||||
'preview.failed': 'Não foi possível carregar o plano',
|
||||
'preview.invalidAddress': 'Endereço do plano inválido',
|
||||
'preview.historyUnavailable': 'O histórico da sessão não está disponível',
|
||||
'preview.notFound': 'Este plano não foi encontrado',
|
||||
'preview.unavailable': 'A visualização do plano não está disponível',
|
||||
'preview.expired': 'Esta visualização temporária do plano expirou. Reabra-o no cartão de revisão pendente.',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/** Read immutable plan arguments from a Session snapshot and earlier history pages. */
|
||||
/** Read immutable plan arguments from a Session history page and earlier pages. */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { SessionFollowFrame } from '@deepseek-ai/dsh-api-session-controller/types'
|
||||
import type { ResourceProvider } from '@deepseek-ai/dsh-client-resources/client'
|
||||
import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
|
||||
@@ -10,8 +9,6 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface RemoteErrorDetailsMap {
|
||||
/** The resource URL does not identify a plan invocation. */
|
||||
'plan/invalid-address': Record<string, never>
|
||||
/** Session history ended before an opening snapshot. */
|
||||
'plan/unavailable': Record<string, never>
|
||||
/** The Session has no readable plan for this invocation. */
|
||||
'plan/not-found': Record<string, never>
|
||||
/** The Session history read failed. */
|
||||
@@ -28,13 +25,13 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
|
||||
/**
|
||||
* Bind plan reads to the generated Session Remote face.
|
||||
* Opening a follow reads projections and may activate a prepared Session on the Host.
|
||||
* Generated Remote streams can throw carrier failures; the provider reports failed
|
||||
* reads as resource failure frames and preserves Remote error codes.
|
||||
* A plan is a logged tool-call argument, so the provider reads one page at the current log
|
||||
* end and pages backwards from its cut. It neither activates nor subscribes.
|
||||
* Remote failures are reported as resource failure frames with their original codes.
|
||||
* @param remote - Existing Session history API.
|
||||
* @returns a provider whose reads stop after finding the exact invocation.
|
||||
*/
|
||||
export function planResourceProvider(remote: Pick<Context['remote']['session'], 'follow' | 'page'>): ResourceProvider<'plan'> {
|
||||
export function planResourceProvider(remote: Pick<Context['remote']['session'], 'events' | 'page'>): ResourceProvider<'plan'> {
|
||||
return {
|
||||
protocol: 'plan',
|
||||
async *open(address, { signal }): AsyncIterable<RemoteResult<SubmittedPlan>> {
|
||||
@@ -47,14 +44,12 @@ export function planResourceProvider(remote: Pick<Context['remote']['session'],
|
||||
}
|
||||
const sessionAddress = target.session
|
||||
try {
|
||||
// The snapshot supplies the page API's fixed log cut. Breaking closes the follow stream.
|
||||
let snapshot: Extract<SessionFollowFrame, { type: 'snapshot' }> | undefined
|
||||
for await (const frame of remote.follow({ address: sessionAddress }, signal)) {
|
||||
if (frame.type === 'snapshot') { snapshot = frame; break }
|
||||
}
|
||||
// One page fixes the cut the later pages page back from.
|
||||
const first = await remote.events({ address: sessionAddress }, signal)
|
||||
if (aborted()) return
|
||||
if (snapshot === undefined) throw new RemoteError('plan/unavailable', 'Session history ended before the plan could be read.', {})
|
||||
let page = { records: snapshot.records, hasMore: snapshot.hasMore }
|
||||
if (!first.ok) { yield { ok: false, error: first.error }; return }
|
||||
const throughSeq = first.value.cursor
|
||||
let page = { records: first.value.records, hasMore: first.value.hasMore }
|
||||
while (true) {
|
||||
for (const entry of page.records) {
|
||||
const plan = submittedPlan(entry.event)
|
||||
@@ -65,7 +60,7 @@ export function planResourceProvider(remote: Pick<Context['remote']['session'],
|
||||
}
|
||||
const beforeSeq = page.records[0]?.event.seq
|
||||
if (!page.hasMore || beforeSeq === undefined) break
|
||||
const next = await remote.page({ address: sessionAddress, throughSeq: snapshot.cursor, beforeSeq }, signal)
|
||||
const next = await remote.page({ address: sessionAddress, throughSeq, beforeSeq }, signal)
|
||||
if (aborted()) return
|
||||
if (!next.ok) { yield next; return }
|
||||
page = next.value
|
||||
|
||||
@@ -333,7 +333,6 @@ describe('plan entry points and document', () => {
|
||||
expect(screen.getByRole('status').textContent).toBe(dictionary['preview.unavailable'])
|
||||
for (const [code, message] of [
|
||||
['plan/invalid-address', dictionary['preview.invalidAddress']],
|
||||
['plan/unavailable', dictionary['preview.historyUnavailable']],
|
||||
['plan/not-found', dictionary['preview.notFound']],
|
||||
['plan/read-failed', 'connection lost'],
|
||||
['session/not-found', 'connection lost'],
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SessionEventEntry, SessionEventsPage } from '@deepseek-ai/dsh-api-session-controller/types'
|
||||
import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { planResourceProvider } from '../src/client/plan-resource.ts'
|
||||
|
||||
const address = 'dsh-resource://plan/session/call'
|
||||
const event = { type: 'tool/call', seq: 1, data: { callId: 'call', name: 'exit_plan_mode', arguments: '{"plan":"# Saved plan"}' } }
|
||||
const entry = { type: 'event', event }
|
||||
const event: SessionEventEntry['event'] = {
|
||||
type: 'tool/call', seq: 1, time: 1,
|
||||
data: { callId: 'call', name: 'exit_plan_mode', arguments: '{"plan":"# Saved plan"}' },
|
||||
}
|
||||
const entry: SessionEventEntry = { type: 'event', event }
|
||||
const olderUserEntry: SessionEventEntry = {
|
||||
type: 'event', event: { type: 'user/message', seq: 80, time: 1, data: {} },
|
||||
}
|
||||
|
||||
function setup(records: unknown[], hasMore = false) {
|
||||
const closed = vi.fn()
|
||||
const follow = vi.fn(async function* () {
|
||||
try { yield { type: 'snapshot', cursor: 100, records, hasMore } }
|
||||
finally { closed() }
|
||||
})
|
||||
const page = vi.fn(async () => ({ ok: true, value: { records: [entry], hasMore: false } }))
|
||||
const provider = planResourceProvider({ follow, page } as unknown as Parameters<typeof planResourceProvider>[0])
|
||||
return { provider, follow, page, closed }
|
||||
type SessionRemote = Parameters<typeof planResourceProvider>[0]
|
||||
|
||||
function setup(records: SessionEventsPage['records'], hasMore = false) {
|
||||
const opening: SessionEventsPage = { cursor: 100, records, hasMore }
|
||||
const events = vi.fn<SessionRemote['events']>(async () => ({ ok: true, value: opening }))
|
||||
const page = vi.fn<SessionRemote['page']>(async () => ({
|
||||
ok: true, value: { cursor: 100, records: [entry], hasMore: false },
|
||||
}))
|
||||
const provider = planResourceProvider({ events, page })
|
||||
return { provider, events, page }
|
||||
}
|
||||
|
||||
async function read(provider: ReturnType<typeof planResourceProvider>, url = address, signal = new AbortController().signal) {
|
||||
@@ -24,82 +32,95 @@ async function read(provider: ReturnType<typeof planResourceProvider>, url = add
|
||||
}
|
||||
|
||||
describe('plan history resource', () => {
|
||||
it('restores the exact invocation and releases its snapshot stream', async () => {
|
||||
it('restores the exact invocation from one page', async () => {
|
||||
const b = setup([entry])
|
||||
expect(await read(b.provider)).toEqual([{ ok: true, value: { callId: 'call', markdown: '# Saved plan', title: 'Saved plan' } }])
|
||||
expect(b.closed).toHaveBeenCalledOnce()
|
||||
expect(b.events).toHaveBeenCalledWith({ address: { kind: 'session', sessionId: 'session' } }, expect.any(AbortSignal))
|
||||
expect(b.page).not.toHaveBeenCalled()
|
||||
})
|
||||
it('pages older history with the fixed opening cursor without changing the chat window', async () => {
|
||||
const b = setup([{ type: 'event', event: { type: 'user/message', seq: 80, data: {} } }], true)
|
||||
const b = setup([olderUserEntry], true)
|
||||
expect((await read(b.provider))[0]).toMatchObject({ ok: true })
|
||||
expect(b.page).toHaveBeenCalledWith({ address: { kind: 'session', sessionId: 'session' }, throughSeq: 100, beforeSeq: 80 }, expect.any(AbortSignal))
|
||||
expect(b.closed).toHaveBeenCalledOnce()
|
||||
expect(b.events).toHaveBeenCalledWith({ address: { kind: 'session', sessionId: 'session' } }, expect.any(AbortSignal))
|
||||
expect(b.page).toHaveBeenLastCalledWith({ address: { kind: 'session', sessionId: 'session' }, throughSeq: 100, beforeSeq: 80 }, expect.any(AbortSignal))
|
||||
})
|
||||
it.each(['one-shot', 'continuable', 'unknown'] as const)('restores a %s subagent plan with its complete parent address on every page', async (mode) => {
|
||||
const b = setup([{ type: 'event', event: { type: 'user/message', seq: 80, data: {} } }], true)
|
||||
const address = `dsh-resource://plan/subagent/parent/child/${mode}/call`
|
||||
const b = setup([olderUserEntry], true)
|
||||
const url = `dsh-resource://plan/subagent/parent/child/${mode}/call`
|
||||
const session = { kind: 'subagent', parentSessionId: 'parent', childSessionId: 'child', mode }
|
||||
expect(await read(b.provider, address)).toMatchObject([{ ok: true, value: { title: 'Saved plan' } }])
|
||||
expect(b.follow).toHaveBeenCalledWith({ address: session }, expect.any(AbortSignal))
|
||||
expect(b.page).toHaveBeenCalledWith({ address: session, throughSeq: 100, beforeSeq: 80 }, expect.any(AbortSignal))
|
||||
expect(b.closed).toHaveBeenCalledOnce()
|
||||
expect(await read(b.provider, url)).toMatchObject([{ ok: true, value: { title: 'Saved plan' } }])
|
||||
expect(b.events).toHaveBeenCalledWith({ address: session }, expect.any(AbortSignal))
|
||||
expect(b.page).toHaveBeenLastCalledWith({ address: session, throughSeq: 100, beforeSeq: 80 }, expect.any(AbortSignal))
|
||||
})
|
||||
it('reports missing plans and invalid saved addresses', async () => {
|
||||
const b = setup([])
|
||||
expect(await read(b.provider)).toMatchObject([{ ok: false, error: { code: 'plan/not-found' } }])
|
||||
expect(await read(b.provider, 'dsh-resource://plan/s/%')).toMatchObject([{ ok: false, error: { code: 'plan/invalid-address' } }])
|
||||
expect(b.follow).toHaveBeenCalledOnce()
|
||||
expect(b.events).toHaveBeenCalledOnce()
|
||||
})
|
||||
it('does not publish a response after cancellation', async () => {
|
||||
const b = setup([entry])
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
expect(await read(b.provider, address, controller.signal)).toEqual([])
|
||||
expect(b.follow).not.toHaveBeenCalled()
|
||||
expect(b.events).not.toHaveBeenCalled()
|
||||
})
|
||||
it('reports stream failures and closes an empty stream', async () => {
|
||||
it('reports page failures with their Remote code', async () => {
|
||||
const b = setup([])
|
||||
b.follow.mockImplementationOnce(async function* () { throw new Error('connection lost') })
|
||||
expect(await read(b.provider)).toMatchObject([{ ok: false, error: { code: 'plan/read-failed', message: 'connection lost' } }])
|
||||
b.follow.mockImplementationOnce(async function* () {})
|
||||
expect(await read(b.provider)).toMatchObject([{ ok: false, error: { code: 'plan/unavailable' } }])
|
||||
const error = new RemoteError('plan/read-failed', 'connection lost', {})
|
||||
b.events.mockResolvedValueOnce({ ok: false, error })
|
||||
expect(await read(b.provider)).toEqual([{ ok: false, error }])
|
||||
})
|
||||
it('preserves Remote failures from paging and stream reads', async () => {
|
||||
const b = setup([{ type: 'event', event: { type: 'user/message', seq: 80, data: {} } }], true)
|
||||
const error = new RemoteError('plan/unavailable', 'history unavailable', {})
|
||||
b.page.mockResolvedValueOnce({ ok: false, error } as never)
|
||||
it('preserves Remote failures from paging', async () => {
|
||||
const b = setup([olderUserEntry], true)
|
||||
const error = new RemoteError('plan/read-failed', 'history unavailable', {})
|
||||
b.page.mockResolvedValueOnce({ ok: true, value: { cursor: 100, records: [olderUserEntry], hasMore: true } })
|
||||
b.page.mockResolvedValueOnce({ ok: false, error })
|
||||
expect(await read(b.provider)).toEqual([{ ok: false, error }])
|
||||
b.follow.mockImplementationOnce(async function* () { throw error })
|
||||
expect(await read(b.provider)).toEqual([{ ok: false, error }])
|
||||
b.follow.mockImplementationOnce(async function* () { throw 'disconnected' })
|
||||
})
|
||||
it('reports a thrown page failure as plan/read-failed', async () => {
|
||||
const b = setup([olderUserEntry], true)
|
||||
b.page.mockResolvedValueOnce({ ok: true, value: { cursor: 100, records: [olderUserEntry], hasMore: true } })
|
||||
b.page.mockRejectedValueOnce(new Error('connection lost'))
|
||||
expect(await read(b.provider)).toMatchObject([{ ok: false, error: { code: 'plan/read-failed', message: 'connection lost' } }])
|
||||
})
|
||||
it('stringifies a non-Error page failure', async () => {
|
||||
const b = setup([olderUserEntry], true)
|
||||
b.page.mockResolvedValueOnce({ ok: true, value: { cursor: 100, records: [olderUserEntry], hasMore: true } })
|
||||
b.page.mockRejectedValueOnce('disconnected')
|
||||
expect(await read(b.provider)).toMatchObject([{ ok: false, error: { code: 'plan/read-failed', message: 'disconnected' } }])
|
||||
})
|
||||
it('waits for a snapshot and discards it when cancellation occurs during stream closure', async () => {
|
||||
it('preserves a Remote failure thrown by page', async () => {
|
||||
const b = setup([olderUserEntry], true)
|
||||
const error = new RemoteError('plan/read-failed', 'history unavailable', {})
|
||||
b.page.mockResolvedValueOnce({ ok: true, value: { cursor: 100, records: [olderUserEntry], hasMore: true } })
|
||||
b.page.mockRejectedValueOnce(error)
|
||||
expect(await read(b.provider)).toEqual([{ ok: false, error }])
|
||||
})
|
||||
it('discards an opening page that settles after cancellation', async () => {
|
||||
const b = setup([entry])
|
||||
const controller = new AbortController()
|
||||
b.follow.mockImplementationOnce(async function* () {
|
||||
try {
|
||||
yield entry as never
|
||||
yield { type: 'snapshot', cursor: 100, records: [entry], hasMore: false }
|
||||
} finally { b.closed(); controller.abort() }
|
||||
})
|
||||
expect(await read(b.provider, address, controller.signal)).toEqual([])
|
||||
expect(b.closed).toHaveBeenCalledOnce()
|
||||
expect(b.page).not.toHaveBeenCalled()
|
||||
const started = Promise.withResolvers<undefined>()
|
||||
const pending = Promise.withResolvers<Awaited<ReturnType<typeof b.events>>>()
|
||||
b.events.mockImplementationOnce(() => { started.resolve(undefined); return pending.promise })
|
||||
const result = read(b.provider, address, controller.signal)
|
||||
await started.promise
|
||||
controller.abort()
|
||||
pending.resolve({ ok: true, value: { cursor: 100, records: [entry], hasMore: false } })
|
||||
expect(await result).toEqual([])
|
||||
})
|
||||
it.each(['resolve', 'reject'] as const)('discards a page that settles after cancellation: %s', async (settlement) => {
|
||||
const b = setup([{ type: 'event', event: { type: 'user/message', seq: 80, data: {} } }], true)
|
||||
it.each(['resolve', 'reject'] as const)('discards an older page that settles after cancellation: %s', async (settlement) => {
|
||||
const b = setup([olderUserEntry], true)
|
||||
const controller = new AbortController()
|
||||
const started = Promise.withResolvers<undefined>()
|
||||
const pending = Promise.withResolvers<Awaited<ReturnType<typeof b.page>>>()
|
||||
b.page.mockResolvedValueOnce({ ok: true, value: { cursor: 100, records: [olderUserEntry], hasMore: true } })
|
||||
b.page.mockImplementationOnce(() => { started.resolve(undefined); return pending.promise })
|
||||
const result = read(b.provider, address, controller.signal)
|
||||
await started.promise
|
||||
controller.abort()
|
||||
if (settlement === 'resolve') pending.resolve({ ok: true, value: { records: [entry], hasMore: false } })
|
||||
if (settlement === 'resolve') pending.resolve({ ok: true, value: { cursor: 100, records: [entry], hasMore: false } })
|
||||
else pending.reject(new Error('cancelled'))
|
||||
expect(await result).toEqual([])
|
||||
expect(b.closed).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user