refactor(agent): own model selection on each agent

This commit is contained in:
_Kerman committed 2026-10-20 21:09:09 +08:00
1 parent 639602e63a
commit a9c006f9c3
237 files changed
+1051 -1734

No files matched your search

@@ -21,8 +21,8 @@
en: 41bdda1ac32df558
zh: aa5d0e325924066b
/session-controller/session-media-references:
en: 2f5524fbfc9446e4
zh: f72c0bc9607007ae
en: 9449c5c3a53031c6
zh: 68d8ad0d3632983a
/session-controller/configuration:
en: 5ae2b340c630897f
zh: 1ffbd61ecf302ec2
+1 -1
View File
@@ -90,7 +90,7 @@ References keep local Session data, scoped Contexts, and history streams alive,
GUI model selection requires the exact provider/model pair in the available catalog; unavailable choices reject with `session/model-unavailable`. Prompt admission retains the saved route without a catalog availability gate, so request execution reports missing credentials or unavailable models. `initializeDefaultModel()` saves the first available account model after login when no other provider has a configured API key; credential checks use configured references independently of model availability. A provider with no available models rejects initialization with `session/provider-models-unavailable`. Availability never substitutes another model or rewrites a Session selection.
A successful `selectModel` response acknowledges the recorded Session selection without waiting for the default profile setting to save. The Agent registry owns its projection; a change applies to the next step while a prepared step retains its captured choice. Default saves run in the background in submission order; a failure logs a warning and leaves the Session selection intact. A Session with no recorded selection or request reads the last successfully saved default through live `AgentOptions` accessors. An explicit choice is recorded even when it equals that default.
A successful `selectModel` response acknowledges the recorded Session selection without waiting for the default profile setting to save. The loop registers its projection; a change applies to the next step while a prepared step retains its captured choice. Default saves run in the background in submission order; a failure logs a warning and leaves the Session selection intact. A Session with no recorded selection or request reads the last successfully saved default through live `AgentOptions` accessors. An explicit choice is recorded even when it equals that default.
-----
+1 -1
View File
@@ -90,7 +90,7 @@ Client 的首次 `follow`、重连首屏与 `loadOlder()` 至少请求 50 条以
GUI 模型选择要求确切提供方/模型对出现在可用目录中;不可用的选择以 `session/model-unavailable` 拒绝。提示词准入保留已保存的路由,不按目录可用性阻断发送,由请求执行报告凭据缺失或模型不可用。`initializeDefaultModel()` 在账号登录后、其他提供方均未配置 API key 时,将第一个可用账号模型保存为默认模型;凭据检查使用已配置的引用,不依赖模型是否可用。提供方没有可用模型时,初始化以 `session/provider-models-unavailable` 拒绝。可用性变化不会替换模型或改写会话选择。
`selectModel` 成功返回表示会话模型选择已记录,不等待默认 profile 设置保存。Agent 注册表拥有该投影;变更从下一个步骤开始生效,已准备的步骤保留捕获的选择。默认设置在后台按提交顺序保存;保存失败会记录警告,并保留会话选择。尚未记录选择或请求的会话通过 `AgentOptions` 的动态访问器读取最近一次成功保存的默认值。显式选择即使与默认值相同也会记录。
`selectModel` 成功返回表示会话模型选择已记录,不等待默认 profile 设置保存。循环注册该投影;变更从下一个步骤开始生效,已准备的步骤保留捕获的选择。默认设置在后台按提交顺序保存;保存失败会记录警告,并保留会话选择。尚未记录选择或请求的会话通过 `AgentOptions` 的动态访问器读取最近一次成功保存的默认值。显式选择即使与默认值相同也会记录。
-----
+2 -4
View File
@@ -108,8 +108,7 @@
"@deepseek-ai/dsh-util-values": "workspace:*",
"@deepseek-ai/dsh-util-workspace-path": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-agent-model": "workspace:*"
"@deepseek-ai/dsh-working-directory": "workspace:*"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-session-persistence": {
@@ -167,7 +166,6 @@
"@deepseek-ai/dsh-util-workspace-path": "workspace:*",
"@deepseek-ai/dsh-workspace": "workspace:*",
"@types/mime-types": "^3.0.1",
"@deepseek-ai/dsh-working-directory": "workspace:*",
"@deepseek-ai/dsh-agent-model": "workspace:*"
"@deepseek-ai/dsh-working-directory": "workspace:*"
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ import { mkdir } from 'node:fs/promises'
import type { Context } from '@deepseek-ai/cordis'
import type { Agent, AgentOptions, AgentSetup } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-default-model'
import * as ModelSwitchNotice from '@deepseek-ai/dsh-agent-model/notice'
import { installModelSelection } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-preset-registry'
import { brandString } from '@deepseek-ai/dsh-brand'
import type { Session, SessionId } from '@deepseek-ai/dsh-session'
@@ -306,13 +306,13 @@ export class ApiSessionAgentController {
async composeAgent(presetId: string | undefined): Promise<ApiSessionComposition> {
const presets = this.ctx.get('agentPresets')
if (presets === undefined) {
return { setup: async (agentCtx) => { await agentCtx.plugin(ModelSwitchNotice) } }
return { setup: (agentCtx) => { installModelSelection(agentCtx) } }
}
const resolvedId = (await presets.resolve(presetId)).id
return {
agentPreset: resolvedId,
setup: async (agentCtx) => {
await agentCtx.plugin(ModelSwitchNotice)
installModelSelection(agentCtx)
await presets.mount(agentCtx, resolvedId)
},
}
@@ -173,7 +173,7 @@ export class SessionCommandController {
? {}
: { reasoningEffort: resolved.reasoningEffort }),
}
this.ctx.agentModel.select(agent, selected)
agent.model.select(selected)
void this.ctx.agentDefaultModel.saveSelection(selected).catch((error: unknown) => {
this.ctx.logger.warn(
`session-controller: model selection changed for the Session but the default was not saved: ${String(error)}`,
@@ -323,7 +323,7 @@ export class SessionCommandController {
const admit = async (): Promise<SessionPromptValue> => {
try {
if (hasImage) {
const current = this.ctx.agentModel.resolveForNextStep(agent)
const current = agent.model.current()
if (current !== undefined) {
const model = await this.ctx.llm.resolveModelInfo(current.provider, current.model)
if (model.inputModalities !== undefined && !model.inputModalities.includes('image')) {
@@ -3,7 +3,6 @@
import { hostname } from 'node:os'
import { resolve } from 'node:path'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-model'
import type {} from '@deepseek-ai/dsh-fs'
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
@@ -127,7 +126,6 @@ export class SessionController extends TypertRemoteService {
'remoteAccess',
'sessions',
'sessionProjections',
'agentModel',
'sessionQuery',
'typert',
'workspaceRegistry',
+1 -1
View File
@@ -1,6 +1,6 @@
/** Browser-safe request, result, and lifecycle vocabulary for the Session Remote service. */
import type {} from '@deepseek-ai/dsh-agent-model/types'
import type {} from '@deepseek-ai/dsh-agent/types'
import type { NativeFileApplication } from '@deepseek-ai/dsh-native-command/types'
import type {
AttachmentIdType, ImageAttachmentLimits, ImageAttachmentRef, ImageMediaType,
@@ -2,7 +2,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
import { unsupportedModel, unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { Session } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-workspace'
@@ -31,6 +31,7 @@ async function liveAgent(ctx: Context, session: Session): Promise<Agent> {
id: session.id,
options: {},
session,
model: unsupportedModel(),
inbox: unsupportedInbox(),
status: 'idle',
ctx,
@@ -17,7 +17,7 @@ import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/
import { describe, expect, it, vi } from 'vitest'
import { ApiSessionAgentController } from '../src/agent.ts'
import { SessionCommandController } from '../src/commands.ts'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import { unsupportedModel, createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts'
declare module '@deepseek-ai/dsh-llm' {
@@ -92,6 +92,7 @@ async function commandHarness(
send: vi.fn(),
inject: vi.fn(),
session,
model: unsupportedModel(),
inbox,
status: 'running',
ctx,
@@ -1,9 +1,9 @@
import { AgentModel, modelSelectionProjectionDefinition } from '@deepseek-ai/dsh-agent'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { Context, FiberState } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment'
import type {
FileAttachmentRef, ImageAttachmentRef, SaveFileAttachment, SaveFileStreamAttachment,
@@ -38,7 +38,6 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
onTestFinished(() => ctx.fiber.dispose())
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
await ctx.plugin(CommandRuntime)
const session = ctx.sessions.create(SESSION, {
@@ -46,14 +45,17 @@ async function uploadHarness(origin?: 'subagent'): Promise<{
})
const inbox = createInboxStub()
const followup = vi.fn()
const options = { provider: 'fixture', model: 'fixture-model' }
const projection = await ctx.sessionProjections.register(modelSelectionProjectionDefinition)
const agent = {
id: session.id,
options: { provider: 'fixture', model: 'fixture-model' },
options,
whenIdle: vi.fn(() => Promise.resolve()),
runMaintenance: async () => { throw new Error('unexpected maintenance') },
send: vi.fn(),
inject: vi.fn(),
session,
model: new AgentModel(() => projection.stateOf(session), session, options),
inbox,
status: 'idle',
ctx,
@@ -2,7 +2,6 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { createUserMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
@@ -26,7 +25,6 @@ describe('SessionController facade', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = SessionId('controller-session')
const header: SessionHeader = {
@@ -3,6 +3,7 @@
* metadata-only listing, Agent-free history reads, subagent ownership
* isolation, and prompt failure mapping.
*/
import { AgentModel, modelSelectionProjectionDefinition } from '@deepseek-ai/dsh-agent'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session'
@@ -17,8 +18,7 @@ import { createUserMessage, MessageId } from '@deepseek-ai/dsh-llm'
import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { createInboxStub, mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
import type { Agent, Inbox } from '@deepseek-ai/dsh-agent'
import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
import AttachmentStore from '@deepseek-ai/dsh-attachment'
import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts'
import {
@@ -84,7 +84,6 @@ describe('sessions.list cold merge', () => {
try {
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const historical = header('format-historical', 100)
const current = header('format-current', 200)
@@ -251,7 +250,6 @@ describe('attached updatedAt tracks human prompts', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
await new Promise(resolve => setTimeout(resolve, 0))
@@ -397,7 +395,6 @@ describe('Remote Agent and Session lookup policy', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const remote = await createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
@@ -419,7 +416,6 @@ describe('Remote Agent and Session lookup policy', () => {
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-remote-cold')
const meta = header(sessionId, 1000)
@@ -461,7 +457,6 @@ describe('Remote Agent and Session lookup policy', () => {
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const coldId = sid('session-remote-cold-child')
const coldMeta = header(coldId, 1000, {
@@ -507,7 +502,6 @@ describe('Remote Agent and Session lookup policy', () => {
await ctx.plugin(TypertRegistry)
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-remote-resumed-child')
const meta = header(sessionId, 1000)
@@ -544,7 +538,6 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-child')
const meta = header('session-child', 1000, {
@@ -624,7 +617,6 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('session-legacy-child')
const meta = header('session-legacy-child', 1000, {
@@ -663,7 +655,6 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const parentSession = ctx.sessions.create(sid('session-parent'), { meta: { cwd: '/proj' } })
const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent
@@ -724,7 +715,6 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-ordinary-fork'), {
seed: [{
@@ -737,9 +727,14 @@ describe('subagent ownership fence', () => {
inheritedEventCount: SessionLogOffset(1),
})
const followup = vi.fn()
const agent = {
id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup,
} as unknown as Agent
const agent: Agent = {
id: session.id, session, model: await modelFor(ctx, session),
options: { provider: 'p', model: 'm' },
inbox: inboxFor(), status: 'idle', ctx, followup,
cancel: vi.fn(), whenIdle: () => Promise.resolve(),
runMaintenance: async () => { throw new Error('unexpected maintenance') },
send: vi.fn(), inject: vi.fn(), steer: vi.fn(),
}
await ctx.agents.register(agent)
const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
@@ -756,13 +751,17 @@ describe('subagent ownership fence', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-browser-zone'), { meta: { cwd: '/proj' } })
const followup = vi.fn()
const agent = {
id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup,
} as unknown as Agent
const agent: Agent = {
id: session.id, session, model: await modelFor(ctx, session),
options: { provider: 'p', model: 'm' },
inbox: inboxFor(), status: 'idle', ctx, followup,
cancel: vi.fn(), whenIdle: () => Promise.resolve(),
runMaintenance: async () => { throw new Error('unexpected maintenance') },
send: vi.fn(), inject: vi.fn(), steer: vi.fn(),
}
await ctx.agents.register(agent)
const remote = await createSessionTestRemote(ctx, {
defaultModelSelection: () => ({ provider: 'p', model: 'm' }),
@@ -829,7 +828,6 @@ describe('degenerate composition (no persistence, no factory)', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
@@ -852,7 +850,6 @@ describe('degenerate composition (no persistence, no factory)', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const inspect = vi.fn()
const stat = vi.fn(() => Promise.resolve(undefined))
@@ -875,25 +872,31 @@ describe('degenerate composition (no persistence, no factory)', () => {
})
})
async function modelFor(ctx: Context, session: Session): Promise<AgentModel> {
const projection = await ctx.sessionProjections.register(modelSelectionProjectionDefinition)
return new AgentModel(() => projection.stateOf(session), session, { provider: 'p', model: 'm' })
}
describe('sessions.prompt synchronous rejection', () => {
it('rejects content without non-whitespace text or an attachment before delivery or Session events', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-empty-prompt'))
const options = { provider: 'p', model: 'm' }
const followup = vi.fn()
const steer = vi.fn()
await ctx.agents.register({
id: session.id,
options: { provider: 'p', model: 'm' },
options,
whenIdle: vi.fn(() => Promise.resolve()),
runMaintenance: async () => { throw new Error('unexpected maintenance') },
send: vi.fn(),
inject: vi.fn(),
cancel: vi.fn(),
session,
model: await modelFor(ctx, session),
inbox: inboxFor(),
status: 'idle',
ctx,
@@ -984,7 +987,6 @@ describe('sessions.prompt synchronous rejection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const session = ctx.sessions.create(sid('session-throwing'))
// A live structural stub whose delivery verbs throw synchronously, the
@@ -992,12 +994,17 @@ describe('sessions.prompt synchronous rejection', () => {
await ctx.agents.register({
id: session.id,
session,
model: await modelFor(ctx, session),
inbox: inboxFor(),
status: 'idle',
ctx,
options: { provider: 'p', model: 'm' },
cancel: vi.fn(), whenIdle: () => Promise.resolve(),
runMaintenance: async () => { throw new Error('unexpected maintenance') },
send: vi.fn(), inject: vi.fn(),
followup: () => { throw new Error('agent "session-throwing" lifecycle disposed') },
steer: () => { throw new Error('agent "session-throwing" lifecycle disposed') },
} as unknown as Agent)
})
const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' })
for (const mode of ['queue', 'steer'] as const) {
@@ -1019,7 +1026,6 @@ describe('sessions.prompt synchronous rejection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const sessionId = sid('race-resume')
const meta: SessionHeader = header('race-resume', 1000)
@@ -6,7 +6,6 @@ import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent'
import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit'
import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { ToolCallId, createMessage, createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
@@ -34,7 +33,6 @@ async function composed(workspaces: readonly Workspace[] = []): Promise<Context>
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
installSessionReadTestServices(ctx)
ctx.provide('workspaceRegistry', { list: () => workspaces, archivedSessionIds: [] } as never)
@@ -665,7 +663,7 @@ describe('sessions.fork', () => {
const child = ctx.agents.get(response.value.sessionId)
if (child === undefined) throw new Error('fork did not publish the child agent')
expect(child.session.snapshotEvents().slice(0, child.session.inheritedEventCount)).toEqual(inherited)
expect(ctx.agentModel.resolveForNextStep(child)).toEqual({
expect(child.model.current()).toEqual({
provider: 'inherited-provider', model: 'inherited-model', reasoningEffort: 'high',
})
await ctx.fiber.dispose()
@@ -4,6 +4,7 @@
* catalog injection, unavailable-model refusals, and the prompt-assembly
* boundary for a running selection change.
*/
import { AgentModel, modelSelectionProjectionDefinition } from '@deepseek-ai/dsh-agent'
import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
@@ -11,7 +12,6 @@ import { describe, expect, it, vi, onTestFinished } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import AttachmentStore from '@deepseek-ai/dsh-attachment'
import LlmRuntime, { LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm'
import type {
@@ -99,7 +99,6 @@ async function harness(logged?: {
await ctx.plugin(SystemPrompt, { personaPrefix: '' })
await ctx.plugin(LlmRuntime)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', [
{ provider: 'deepseek-official', id: 'deepseek-chat', name: 'DeepSeek Chat' },
@@ -128,13 +127,16 @@ async function harness(logged?: {
reason: 'initial',
})
}
const projection = await ctx.sessionProjections.register(modelSelectionProjectionDefinition)
const options = {
get provider() { return ctx.agentDefaultModel.currentSelection().provider },
get model() { return ctx.agentDefaultModel.currentSelection().model },
get reasoningEffort() { return ctx.agentDefaultModel.currentSelection().reasoningEffort },
}
const agent = {
id: session.id,
options: {
get provider() { return ctx.agentDefaultModel.currentSelection().provider },
get model() { return ctx.agentDefaultModel.currentSelection().model },
get reasoningEffort() { return ctx.agentDefaultModel.currentSelection().reasoningEffort },
},
options,
model: new AgentModel(() => projection.stateOf(session), session, options),
session,
status: 'running',
ctx,
@@ -520,7 +522,7 @@ describe('Web session model selection', () => {
model: 'deepseek-reasoner',
reasoningEffort: 'max',
})
expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(selected.selected)
expect(agent.model.current()).toEqual(selected.selected)
expect(agent.session.snapshotEvents().at(-1)).toMatchObject({
type: 'model/selection', data: selected.selected,
})
@@ -581,7 +583,10 @@ describe('Web session model selection', () => {
ctx.agents.setFactory({
async createAgent(ownerCtx, options) {
const session = ctx.sessions.create(options.sessionId)
const agent: Agent = { ...template, id: session.id, session, ctx: ownerCtx, options: options.agentOptions ?? {} }
const agentOptions = options.agentOptions ?? {}
const projection = await ownerCtx.sessionProjections.register(modelSelectionProjectionDefinition)
const model = new AgentModel(() => projection.stateOf(session), session, agentOptions)
const agent: Agent = { ...template, id: session.id, session, ctx: ownerCtx, options: agentOptions, model }
await options.setup?.(ownerCtx, agent)
await ctx.agents.register(agent)
return { agent, dispose: () => Promise.resolve() }
@@ -591,9 +596,9 @@ describe('Web session model selection', () => {
const first = expectValue(await remote.create({ cwd: '/tmp' }))
const second = expectValue(await remote.create({ cwd: '/tmp' }))
const agent = ctx.agents.get(first.sessionId)!
expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(stored)
expect(agent.model.current()).toEqual(stored)
stored = { provider: 'deepseek-official', model: 'deepseek-reasoner' }
expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(stored)
expect(agent.model.current()).toEqual(stored)
const selection = { ...stored, reasoningEffort: ReasoningEffortId('high') }
// The explicit selection is identical to the live fallback, including effort.
stored = selection
@@ -601,7 +606,7 @@ describe('Web session model selection', () => {
expect(agent.session.snapshotEvents().filter(event => event.type === 'model/selection'))
.toMatchObject([{ data: selection }])
expectValue(await remote.selectModel({ sessionId: second.sessionId, provider: 'deepseek-official', model: 'deepseek-chat' }))
expect(ctx.agentModel.resolveForNextStep(agent)).toEqual(selection)
expect(agent.model.current()).toEqual(selection)
expect(currentSelection(ctx, first.sessionId)).toEqual(selection)
})
@@ -634,7 +639,7 @@ describe('Web session model selection', () => {
cwd: '/tmp',
})
expect(ctx.agentModel.resolveForNextStep(agent))
expect(agent.model.current())
.toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' })
await ctx.fiber.dispose()
})
@@ -31,7 +31,6 @@ import {
mountAgentLoopTestDependencies,
mountAgentLoopTestHarness,
} from '@deepseek-ai/dsh-agent-loop-testkit'
import * as AgentModelPlugin from '@deepseek-ai/dsh-agent-model'
import { createSessionTestRemote, testSessionPersistence, type TestSessionRemote } from './test-remote.ts'
const ownedContexts = new Set<Context>()
@@ -163,7 +162,6 @@ describe('session.history projections block', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
const parent = ctx.sessions.create(SessionId('wire-seed-parent'), { meta: { cwd: '/workspace' } })
parent.append('turn/start', { turn: 1 })
@@ -581,7 +579,6 @@ describe('session.list projections column', () => {
await ctx.plugin(StorageDomain, { backend: 'json' })
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
await ctx.sessionProjections.register(privatePromptUnit())
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
@@ -637,7 +634,6 @@ describe('session.list projections column', () => {
await ctx.plugin(StorageDomain, { backend: 'json' })
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(AgentModelPlugin)
await ctx.plugin(AgentRegistry)
await ctx.sessionProjections.register(titleProjectionDefinition)
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
@@ -11,65 +11,24 @@
"src/remote-events.ts"
],
"references": [
{
"path": "../../../vendor/cordis"
},
{
"path": "../gateway/tsconfig.client.json"
},
{
"path": "../../attachment/attachment"
},
{
"path": "../../client/connection/tsconfig.client.json"
},
{
"path": "../../client/file-upload/tsconfig.client.json"
},
{
"path": "../../client/store"
},
{
"path": "../../context/file-reference"
},
{
"path": "../../core/session"
},
{
"path": "../../jobs/jobs"
},
{
"path": "../../llm/llm"
},
{
"path": "../../session/session-projection"
},
{
"path": "../../session/session-title"
},
{
"path": "../../subagent/subagent"
},
{
"path": "../../util/brand"
},
{
"path": "../../util/crypto"
},
{
"path": "../../util/workspace-path"
},
{
"path": "../../workspace/workspace"
},
{
"path": "../../typert/protocol"
},
{
"path": "../../util/native-command"
},
{
"path": "../../core/agent-model"
}
{ "path": "../../../vendor/cordis" },
{ "path": "../gateway/tsconfig.client.json" },
{ "path": "../../attachment/attachment" },
{ "path": "../../client/connection/tsconfig.client.json" },
{ "path": "../../client/file-upload/tsconfig.client.json" },
{ "path": "../../client/store" },
{ "path": "../../context/file-reference" },
{ "path": "../../core/session" },
{ "path": "../../jobs/jobs" },
{ "path": "../../llm/llm" },
{ "path": "../../session/session-projection" },
{ "path": "../../session/session-title" },
{ "path": "../../subagent/subagent" },
{ "path": "../../util/brand" },
{ "path": "../../util/crypto" },
{ "path": "../../util/workspace-path" },
{ "path": "../../workspace/workspace" },
{ "path": "../../typert/protocol" },
{ "path": "../../util/native-command" }
]
}
@@ -114,9 +114,6 @@
},
{
"path": "../../session/working-directory"
},
{
"path": "../../core/agent-model"
}
]
}