refactor: identify and freeze messages at creation

This commit is contained in:
_Kerman
2026-07-28 13:55:59 +08:00
parent c49c0ba497
commit fbf87e660c
345 changed files with 5220 additions and 2901 deletions

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import { ProviderRequestId } from '@deepseek-ai/dsh-llm'
import { createUserMessage, ProviderRequestId , createMessage } from '@deepseek-ai/dsh-llm'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import InvariantService from '@deepseek-ai/dsh-invariants'
import * as RetryInvariant from '@deepseek-ai/dsh-llm-retry/invariant'
@@ -223,8 +223,14 @@ describe('llm-retry invariants', () => {
reset.append('assistant/message', {
turn: 2,
step: 1,
content: [{ type: 'text', text: 'success' }],
provenance: { provider: 'mock', model: 'mock' },
message: createMessage({
role: 'assistant',
content: [{ type: 'text', text: 'success' }],
source: {
kind: 'model',
...{ provider: 'mock', model: 'mock' },
},
}),
}, { surfaceOp: 'append' })
reset.append('step/end', { turn: 2, step: 1 })
reset.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
@@ -241,18 +247,18 @@ describe('llm-retry invariants', () => {
await ctx.plugin(SessionStore)
const missingEnd = ctx.sessions.create(SessionId('retry-invariant-missing-end'))
missingEnd.append('user/message', {
missingEnd.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'idle context' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
appendRetryTurn(missingEnd, 2)
const nonFailureEnd = ctx.sessions.create(SessionId('retry-invariant-non-failure-end'))
nonFailureEnd.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
nonFailureEnd.append('user/message', {
nonFailureEnd.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'idle context' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
}), { surfaceOp: 'append' })
appendRetryTurn(nonFailureEnd, 2)
const missingStart = ctx.sessions.create(SessionId('retry-invariant-missing-start'))

View File

@@ -8,7 +8,7 @@ import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import LlmService, { LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import LlmService, { createUserMessage, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
@@ -105,7 +105,7 @@ describe('real Loader composition', () => {
const adapter = new TransientOnceAdapter()
loaded.llm.registerAdapter(['mock'], adapter)
const agent = loaded.agentLoop.create(SessionId('loader-retry'), { provider: 'mock', model: 'mock' })
agent.followup({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } }))
await agent.whenIdle()
expect(adapter.requests).toBe(2)

View File

@@ -1,7 +1,7 @@
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import type { Fiber } from 'cordis'
import LlmService, { CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import LlmService, { createUserMessage, CallId, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, resolveRetryPolicy } from '@deepseek-ai/dsh-llm'
import type {
AlwaysRetryPolicyConfig,
BackoffConfig,
@@ -190,7 +190,7 @@ describe('provider-routed retry policy', () => {
})
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
const event = await scheduled
expect(event.data).toEqual({
@@ -235,7 +235,7 @@ describe('provider-routed retry policy', () => {
const agent = context.agentLoop.create(SessionId('retry-empty-response'), { provider: 'mock', model: 'mock' })
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
const event = await scheduled
expect(event.data.failure).toEqual({
message: 'model returned a completed response with no content',
@@ -277,7 +277,7 @@ describe('provider-routed retry policy', () => {
const agent = context.agentLoop.create(SessionId('retry-partial'), { provider: 'mock', model: 'mock' })
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await scheduled
const idle = waitForIdle(context, agent)
await vi.advanceTimersByTimeAsync(500)
@@ -316,7 +316,7 @@ describe('provider-routed retry policy', () => {
const agent = context.agentLoop.create(SessionId('retry-exhausted'), { provider: 'mock', model: 'mock' })
const first = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
expect((await first).data.delayMs).toBe(450)
const second = waitForRetry(context, agent, 2)
@@ -347,7 +347,7 @@ describe('provider-routed retry policy', () => {
const agent = context.agentLoop.create(SessionId('retry-zero-delay'), { provider: 'mock', model: 'mock' })
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
expect((await scheduled).data.delayMs).toBe(0)
const idle = waitForIdle(context, agent)
@@ -367,7 +367,7 @@ describe('provider-routed retry policy', () => {
}) }))
const acceptedAgent = context.agentLoop.create(SessionId('retry-after-accepted'), { provider: 'mock', model: 'mock' })
const scheduled = waitForRetry(context, acceptedAgent, 1)
acceptedAgent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
acceptedAgent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
expect((await scheduled).data.delayMs).toBe(2_000)
const acceptedIdle = waitForIdle(context, acceptedAgent)
await vi.advanceTimersByTimeAsync(2_000)
@@ -381,7 +381,7 @@ describe('provider-routed retry policy', () => {
;({ ctx: context } = await harness(rejected))
const rejectedAgent = context.agentLoop.create(SessionId('retry-after-rejected'), { provider: 'mock', model: 'mock' })
const rejectedIdle = waitForIdle(context, rejectedAgent)
rejectedAgent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
rejectedAgent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await rejectedIdle
expect(rejected.requests).toHaveLength(1)
expect(rejectedAgent.session.events.some(event => event.type === 'llm/retry')).toBe(false)
@@ -404,7 +404,7 @@ describe('provider-routed retry policy', () => {
})
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
expect((await scheduled).data.delayMs).toBe(3)
const idle = waitForIdle(context, agent)
await vi.advanceTimersByTimeAsync(3)
@@ -419,7 +419,7 @@ describe('provider-routed retry policy', () => {
;({ ctx: context } = await harness(adapter))
const agent = context.agentLoop.create(SessionId('retry-auth'), { provider: 'mock', model: 'mock' })
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await idle
expect(adapter.requests).toHaveLength(1)
expect(agent.session.events.some(event => event.type === 'llm/retry')).toBe(false)
@@ -437,7 +437,7 @@ describe('provider-routed retry policy', () => {
})
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'missing route' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'missing route' }], source: { kind: 'user' } }))
await idle
expect(adapter.requests).toHaveLength(0)
@@ -464,7 +464,7 @@ describe('provider-routed retry policy', () => {
model: 'mock',
})
const normalIdle = waitForIdle(context, normalAgent)
normalAgent.followup({ content: [{ type: 'text', text: 'normal' }], source: { kind: 'user' } })
normalAgent.followup(createUserMessage({ content: [{ type: 'text', text: 'normal' }], source: { kind: 'user' } }))
await normalIdle
expect(normalAgent.session.events.some(event => event.type === 'llm/retry')).toBe(false)
@@ -473,7 +473,7 @@ describe('provider-routed retry policy', () => {
model: 'mock',
})
const scheduled = waitForRetry(context, alwaysAgent, 1)
alwaysAgent.followup({ content: [{ type: 'text', text: 'always' }], source: { kind: 'user' } })
alwaysAgent.followup(createUserMessage({ content: [{ type: 'text', text: 'always' }], source: { kind: 'user' } }))
expect((await scheduled).data).toMatchObject({
provider: 'other',
mode: 'always',
@@ -507,7 +507,7 @@ describe('provider-routed retry policy', () => {
})
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'reroute' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'reroute' }], source: { kind: 'user' } }))
expect((await scheduled).data).toMatchObject({ provider: 'other', mode: 'always' })
const idle = waitForIdle(context, agent)
await vi.advanceTimersByTimeAsync(1)
@@ -544,10 +544,10 @@ describe('provider-routed retry policy', () => {
})
const idle = waitForIdle(context, agent)
agent.followup({
agent.followup(createUserMessage({
content: [{ type: 'text', text: 'switch provider after failure' }],
source: { kind: 'user' },
})
}))
await vi.runAllTimersAsync()
await idle
@@ -591,10 +591,10 @@ describe('provider-routed retry policy', () => {
model: 'mock',
})
const scheduled = waitForRetry(context, agent, 1)
agent.followup({
agent.followup(createUserMessage({
content: [{ type: 'text', text: 'replace while in flight' }],
source: { kind: 'user' },
})
}))
await entered.promise
mounted.disposeAdapter()
@@ -659,7 +659,7 @@ describe('provider-routed retry policy', () => {
})
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'keep trying' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'keep trying' }], source: { kind: 'user' } }))
await vi.runAllTimersAsync()
await idle
@@ -696,7 +696,7 @@ describe('provider-routed retry policy', () => {
})
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'safe input' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'safe input' }], source: { kind: 'user' } }))
await scheduled
const idle = waitForIdle(context, agent)
await vi.advanceTimersByTimeAsync(1)
@@ -725,7 +725,7 @@ describe('provider-routed retry policy', () => {
})
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } }))
await idle
expect(adapter.requests).toHaveLength(2)
@@ -752,7 +752,7 @@ describe('provider-routed retry policy', () => {
})
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'recover' }], source: { kind: 'user' } }))
await scheduled
const idle = waitForIdle(context, agent)
await vi.advanceTimersByTimeAsync(1)
@@ -771,7 +771,7 @@ describe('provider-routed retry policy', () => {
context = mounted.ctx
const agent = context.agentLoop.create(SessionId('retry-hmr'), { provider: 'mock', model: 'mock' })
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await scheduled
const idle = waitForIdle(context, agent)
@@ -802,7 +802,7 @@ describe('provider-routed retry policy', () => {
model: 'mock',
})
const idle = waitForIdle(context, agent).then(() => { order.push('idle') })
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await entered.promise
const disposing = mounted.retryFiber.dispose().then(() => { order.push('disposed') })
@@ -842,7 +842,7 @@ describe('provider-routed retry policy', () => {
model: 'mock',
})
const idle = waitForIdle(context, agent).then(() => { order.push('idle') })
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await entered.promise
agent.cancel({ kind: 'user' })
@@ -882,7 +882,7 @@ describe('provider-routed retry policy', () => {
})
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await entered.promise
let timer: ReturnType<typeof setTimeout> | undefined
const outcome = await Promise.race([
@@ -929,7 +929,7 @@ describe('provider-routed retry policy', () => {
model: 'mock',
})
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await captured.promise
await mounted.retryFiber.dispose()
@@ -950,7 +950,7 @@ describe('provider-routed retry policy', () => {
;({ ctx: context } = await harness(adapter, { mock: alwaysConfig() }))
const agent = context.agentLoop.create(SessionId('retry-cancel'), { provider: 'mock', model: 'mock' })
const scheduled = waitForRetry(context, agent, 1)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await scheduled
const idle = waitForIdle(context, agent)
agent.cancel({ kind: 'user' })
@@ -984,7 +984,7 @@ describe('provider-routed retry policy', () => {
const agent = context.agentLoop.create(SessionId('retry-pre-cancel'), { provider: 'mock', model: 'mock' })
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await idle
expect(adapter.requests).toHaveLength(1)
@@ -1008,7 +1008,7 @@ describe('provider-routed retry policy', () => {
})
const idle = waitForIdle(context, agent)
agent.followup({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } }))
await idle
expect(adapter.requests).toHaveLength(1)

View File

@@ -1,3 +1,4 @@
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import { createServer } from 'node:http'
import type { AddressInfo } from 'node:net'
import { afterEach, describe, expect, it } from 'vitest'
@@ -66,7 +67,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise<void> {
function sendAndWait(ctx: Context, agent: Agent): Promise<void> {
const idle = waitForIdle(ctx, agent)
agent.followup({ content: [{ type: 'text', text: 'recover through the provider boundary' }], source: { kind: 'user' } })
agent.followup(createUserMessage({ content: [{ type: 'text', text: 'recover through the provider boundary' }], source: { kind: 'user' } }))
return idle
}