diff --git a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts index 8d762591eb..021fddb842 100644 --- a/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts +++ b/packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts @@ -5,7 +5,7 @@ import { createUserMessage, createSystemMessage, CONTEXT_WINDOW_EXCEEDED_CODE, L import type { ContentBlock, GenerateOptions, LlmResolvedModelInfo, ResolvedRetryPolicy, StreamChunk } from '@deepseek-ai/dsh-llm' import { ToolCallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' -import type { Agent } from '@deepseek-ai/dsh-agent' +import { recordModelSelection, type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic' @@ -205,16 +205,14 @@ function overflowHistorySeed(): readonly SessionEvent[] { } describe('CBR-001: a real-loop checkpoint is a valid boundary on both sides', () => { - it('uses the model actually routed by agent/request for post-step pressure', async () => { + it('uses the recorded model selection for post-step pressure', async () => { const { ctx } = await harness(8) - ctx.on('agent/request', async (_payload, next) => ({ - ...await next(), provider: 'mock', model: 'mock', - })) try { const agent = await ctx.agentLoop.create(SessionId('routed-pressure'), { provider: 'unconfigured-agent-fallback', model: 'unconfigured-agent-fallback', }) + recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'do a routed multi-step task' }], source: { kind: 'user' } })) await waitForIdle(ctx, agent) @@ -380,9 +378,6 @@ describe('context-overflow recovery across the real loop and compaction-basic', await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(TokenMeter) ctx.llm.registerAdapter(['mock'], adapter) - ctx.on('agent/request', async (_payload, next) => ({ - ...await next(), provider: 'mock', model: 'mock', - })) await ctx.plugin(BasicCompactionEngine, { headroomTokens: 0, thresholdRatio: 1, @@ -402,6 +397,7 @@ describe('context-overflow recovery across the real loop and compaction-basic', }, }) + recordModelSelection(ctx.sessionProjections, agent, { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'continue from history' }], source: { kind: 'user' } })) await agent.whenIdle() diff --git a/packages/context/file-reference-local/tests/service.spec.ts b/packages/context/file-reference-local/tests/service.spec.ts index 19ec85c3f6..88cef08184 100644 --- a/packages/context/file-reference-local/tests/service.spec.ts +++ b/packages/context/file-reference-local/tests/service.spec.ts @@ -26,9 +26,9 @@ async function harness(): Promise { await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt, { personaPrefix: '' }) await ctx.plugin(ToolRegistry) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(WorkingDirectory) return ctx } @@ -64,6 +64,7 @@ describe('LocalFileReferenceService', () => { it('defers guidance until optional prompt and tool services become available', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) ctx.provide('workingDirectory', { ensure: vi.fn() }) await ctx.plugin(LocalFileReferenceService) diff --git a/packages/experimental/browser-use-runtime/tests/mcp.spec.ts b/packages/experimental/browser-use-runtime/tests/mcp.spec.ts index d9c4465447..b4fc9d4b36 100644 --- a/packages/experimental/browser-use-runtime/tests/mcp.spec.ts +++ b/packages/experimental/browser-use-runtime/tests/mcp.spec.ts @@ -15,6 +15,7 @@ import Llm, { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import Sessions, { SessionId } from '@deepseek-ai/dsh-session' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import Agents from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' @@ -148,6 +149,7 @@ it('waits for SystemPrompt and ToolRuntime before reserving the provider', async const ctx = new Context() contexts.push(ctx) await ctx.plugin(BrowserUse) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(Agents) provideWorkingDirectoryFixture(ctx) const tools = ctx.plugin(Tools) diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts index 273657e579..37181228d7 100644 --- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts +++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts @@ -6,6 +6,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import AgentRegistry from '@deepseek-ai/dsh-agent' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandRuntime from '@deepseek-ai/dsh-commands' @@ -56,6 +57,7 @@ describe('/feedback real Loader composition through cordis.yml', () => { vi.stubEnv('DSH_HOME', root) const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ + "- name: '@deepseek-ai/dsh-session-projection'", "- name: '@deepseek-ai/dsh-agent'", "- name: '@deepseek-ai/dsh-session'", "- name: '@deepseek-ai/dsh-commands'", @@ -68,6 +70,7 @@ describe('/feedback real Loader composition through cordis.yml', () => { await context.plugin(Loader) context.loader.builtins.include = Include const modules = new Map([ + ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-commands', CommandRuntime], diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts index 74b8ce5dd6..710939691f 100644 --- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts +++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts @@ -76,13 +76,11 @@ async function setup( contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) if (options.sandboxMode === undefined) { await ctx.plugin(LocalFileSystem, { cwd: root }) } else { - // SandboxPolicy declares the registry as a required injection; mount it - // before the policy activates. - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicy, { mode: options.sandboxMode, workspaceRoot: root }) await ctx.plugin(SandboxedFileSystem, { cwd: root }) } @@ -616,6 +614,7 @@ describe('tool-str-replace-editor', () => { contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem, { cwd: root }) Object.defineProperty(ctx.fs, 'sandboxMode', { value: 'read-only' }) diff --git a/packages/interaction/user-questions/tests/user-questions.spec.ts b/packages/interaction/user-questions/tests/user-questions.spec.ts index f0f744259a..3fa947fa4c 100644 --- a/packages/interaction/user-questions/tests/user-questions.spec.ts +++ b/packages/interaction/user-questions/tests/user-questions.spec.ts @@ -198,6 +198,7 @@ describe('UserQuestionService', () => { it('rejects a live runtime-owned agent before reaching the provider', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const p = { ask: vi.fn(async () => ({ answers: [] })) } @@ -220,6 +221,7 @@ describe('UserQuestionService', () => { it('reaches the provider for a lineage-bearing session resumed as a runtime root', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const p = provider('yes') @@ -250,6 +252,7 @@ describe('UserQuestionService', () => { it('rejects a stale agent object that reuses a live id', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const p = { ask: vi.fn(async () => ({ answers: [] })) } @@ -389,8 +392,8 @@ function liveAgent(id: string): LiveAgent { async function timedContext(): Promise { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) return ctx } @@ -832,16 +835,15 @@ describe('late replies', () => { await ctx.fiber.dispose() }) - it('answers nothing when the composition carries no Session projections', async () => { + it('rejects a late reply without a live Agent registry', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const agent = liveAgent('late-no-projections') - ctx.agents.enter(agent, undefined) askInLog(agent, timedCallId) continueInLog(agent, timedCallId) - expect(ctx.userQuestions.answer(agent, timedCallId, { answers: [] })).toBe(false) + expect(() => ctx.userQuestions.answer(agent, timedCallId, { answers: [] })) + .toThrow(expect.objectContaining({ code: 'CALLER_NOT_LIVE' })) expect(agent.steer).not.toHaveBeenCalled() await ctx.fiber.dispose() }) diff --git a/packages/llm/llm-deepseek/tests/account-routing.spec.ts b/packages/llm/llm-deepseek/tests/account-routing.spec.ts index 1fa0c7cb01..afdc4e5516 100644 --- a/packages/llm/llm-deepseek/tests/account-routing.spec.ts +++ b/packages/llm/llm-deepseek/tests/account-routing.spec.ts @@ -3,7 +3,7 @@ import { afterEach, expect, it, vi } from 'vitest' import { installAccountTaskCancellation, type DeepSeekAccount } from '@deepseek-ai/dsh-deepseek-account' import { Context } from '@deepseek-ai/cordis' import LlmRuntime, { createUserMessage, LlmAdapter, type GenerateOptions, type StreamChunk, ToolCallId } from '@deepseek-ai/dsh-llm' -import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { recordModelSelection, type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' @@ -95,7 +95,7 @@ it.each(['deepseek-account', 'deepseek-official'])('cancels only the active acco ? { kind: 'hook', reason: 'deepseek-account/signed-out' } : { kind: 'user' } } }) }) -it.each(['tool', 'retry'])('replaces the active provider only after the next request binds: %s', async (transition) => { +it.each(['tool', 'retry'])('keeps the active provider until a later step binds a new selection: %s', async (transition) => { const ctx = await harness() const preparing = Promise.withResolvers() const proceed = Promise.withResolvers() @@ -106,11 +106,18 @@ it.each(['tool', 'retry'])('replaces the active provider only after the next req if (++requests === 1) return config preparing.resolve(undefined) await proceed.promise - return { ...config, provider: 'deepseek-official' } + return config + }) + const selectOfficial = (agent: Agent) => { + recordModelSelection(ctx.sessionProjections, agent, { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) + } + ctx.on('agent/request-error', async ({ agent }, next) => { + await next() + selectOfficial(agent) + return { kind: 'retry' } }) - ctx.on('agent/request-error', async (_event, next) => { await next(); return { kind: 'retry' } }) ctx.on('llm/stream', async function* (request) { - if (request.provider === 'deepseek-account') { + if (requests === 1) { if (transition === 'retry') { yield { type: 'finish', reason: { kind: 'error', failure: { code: 'TRANSPORT', message: 'retry fixture' } } } return @@ -122,22 +129,29 @@ it.each(['tool', 'retry'])('replaces the active provider only after the next req } const signal = request.signal! streaming.resolve(signal) - await new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) }) + if (!signal.aborted) await new Promise((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) }) signal.throwIfAborted() }) - ctx.tools.register(defineContentToolFixture({ name: 'continue', description: '', parameters: {}, execute: async () => [{ type: 'text', text: 'done' }] })) + ctx.tools.register(defineContentToolFixture({ name: 'continue', description: '', parameters: {}, execute: async () => { + selectOfficial(agent) + return [{ type: 'text', text: 'done' }] + } })) const agent = await ctx.agentLoop.create(SessionId('switch-provider'), { provider: 'deepseek-account', model: 'deepseek-v4-flash' }) expect(agent.session.requestContext()?.provider).toBeUndefined() - send(agent) - await preparing.promise - expect(agent.session.requestContext()?.provider).toBe('deepseek-account') - proceed.resolve(undefined) - const signal = await streaming.promise - expect(agent.session.requestContext()?.provider).toBe('deepseek-official') - ctx.emit('deepseek-account/signed-out') - expect(signal.aborted).toBe(false) - agent.cancel({ kind: 'user' }) - await agent.whenIdle() + try { + send(agent) + await preparing.promise + expect(agent.session.requestContext()?.provider).toBe('deepseek-account') + proceed.resolve(undefined) + const signal = await streaming.promise + expect(agent.session.requestContext()?.provider).toBe(transition === 'tool' ? 'deepseek-official' : 'deepseek-account') + ctx.emit('deepseek-account/signed-out') + expect(signal.aborted).toBe(transition === 'retry') + } finally { + proceed.resolve(undefined) + agent.cancel({ kind: 'user' }) + await agent.whenIdle() + } expect(agent.status).toBe('idle') }) @@ -230,6 +244,7 @@ it('cancels an account child without stopping its API-key parent', async () => { }) it('lists each DeepSeek route only when its own credential is present', async () => { + vi.stubEnv('DEEPSEEK_BASE_URL', 'https://api.deepseek.com') vi.stubEnv('DEEPSEEK_API_KEY', '') vi.stubGlobal('fetch', vi.fn((input: string | URL | Request) => Promise.resolve(listingRequest(input) ?? new Response(null, { status: 500 })))) const ctx = await harness() @@ -264,13 +279,14 @@ it('ignores idle account history but uses the last bound route during a new turn const cancel = vi.spyOn(agent, 'cancel') ctx.emit('deepseek-account/signed-out') expect(cancel).not.toHaveBeenCalled() + recordModelSelection(ctx.sessionProjections, agent, { provider: 'deepseek-official', model: 'deepseek-v4-flash' }) const preparing = Promise.withResolvers() const proceed = Promise.withResolvers() ctx.on('agent/request', async (_event, next) => { const config = await next() preparing.resolve(undefined) await proceed.promise - return { ...config, provider: 'deepseek-official' } + return config }) try { send(agent) diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts index 9ad719a67d..381101c736 100644 --- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts +++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts @@ -9,6 +9,7 @@ import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' import LlmRuntime from '@deepseek-ai/dsh-llm' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import { credentialRef } from '@deepseek-ai/dsh-credentials' @@ -58,6 +59,8 @@ async function loadComposition( " name: '@deepseek-ai/dsh-llm'", '- id: session', " name: '@deepseek-ai/dsh-session'", + '- id: session-projection', + " name: '@deepseek-ai/dsh-session-projection'", '- id: agents', " name: '@deepseek-ai/dsh-agent'", '- id: deepseek-llm-api-extensions', @@ -93,6 +96,7 @@ async function loadComposition( const modules = new Map([ ['@deepseek-ai/dsh-llm', LlmRuntime], ['@deepseek-ai/dsh-session', SessionStore], + ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-deepseek-llm-api-extensions', DeepSeekLlmApiExtensionRegistry], ['@deepseek-ai/dsh-session-log-deepseek', SessionLogDeepSeek], diff --git a/packages/llm/llm-retry/tests/retry.spec.ts b/packages/llm/llm-retry/tests/retry.spec.ts index 3adb88de05..89c6bf7a4f 100644 --- a/packages/llm/llm-retry/tests/retry.spec.ts +++ b/packages/llm/llm-retry/tests/retry.spec.ts @@ -17,7 +17,7 @@ import type { SessionEvent, SessionEventMap } from '@deepseek-ai/dsh-session' import type { LlmRetryEventData } from '@deepseek-ai/dsh-llm-retry/types' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' -import AgentRegistry from '@deepseek-ai/dsh-agent' +import AgentRegistry, { recordModelSelection } from '@deepseek-ai/dsh-agent' import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as retry from '../src/index.ts' @@ -524,7 +524,7 @@ describe('provider-routed retry policy', () => { expect(adapter.requests.map(request => request.provider)).toEqual(['mock', 'other', 'other']) }) - it('selects an always policy from the provider chosen by agent/request', async () => { + it('selects an always policy from the recorded provider', async () => { vi.useFakeTimers() const adapter = new ScriptedAdapter([ new LlmError('rerouted auth failed', 'AUTH'), @@ -532,16 +532,12 @@ describe('provider-routed retry policy', () => { ]) ;({ ctx: context } = await harness(adapter, { other: alwaysConfig({ initialDelayMs: 1, maxDelayMs: 1, jitterRatio: 0 }), - }, (ctx) => { - ctx.on('agent/request', async (_payload, next) => ({ - ...await next(), - provider: 'other', - })) })) const agent = await context.agentLoop.create(SessionId('retry-provider-rerouted'), { provider: 'mock', model: 'mock', }) + recordModelSelection(context.sessionProjections, agent, { provider: 'other', model: 'mock' }) const scheduled = waitForRetry(context, agent, 1) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'reroute' }], source: { kind: 'user' } })) @@ -553,10 +549,15 @@ describe('provider-routed retry policy', () => { expect(adapter.requests.map(request => request.provider)).toEqual(['other', 'other']) }) - it('keeps finite retry budgets scoped to the failed provider', async () => { + it('starts a fresh retry budget when the next step selects another provider', async () => { vi.useFakeTimers() const adapter = new ScriptedAdapter([ new LlmError('mock failed', 'SERVER'), + [ + { type: 'block-start', index: 0, blockType: 'tool-call' }, + { type: 'block-end', index: 0, block: { type: 'tool-call', id: ToolCallId('switch'), name: 'switch_provider', arguments: '{}' } }, + { type: 'finish', reason: { kind: 'tool-calls' } }, + ], new LlmError('other failed', 'SERVER'), textResponse('other recovered'), ]) @@ -569,16 +570,18 @@ describe('provider-routed retry policy', () => { maxRetries: 1, backoff: { initialDelayMs: 1, maxDelayMs: 1 }, }), - }, (ctx) => { - ctx.on('agent/request', async (_payload, next) => ({ - ...await next(), - provider: adapter.requests.length === 0 ? 'mock' : 'other', - })) })) const agent = await context.agentLoop.create(SessionId('retry-provider-budgets'), { provider: 'mock', model: 'mock', }) + context.tools.register(defineContentToolFixture({ + name: 'switch_provider', description: '', parameters: {}, + execute: async () => { + recordModelSelection(agent.ctx.sessionProjections, agent, { provider: 'other', model: 'mock' }) + return [{ type: 'text', text: 'switched' }] + }, + })) const idle = waitForIdle(context, agent) agent.followup(createUserMessage({ @@ -588,7 +591,7 @@ describe('provider-routed retry policy', () => { await vi.runAllTimersAsync() await idle - expect(adapter.requests.map(request => request.provider)).toEqual(['mock', 'other', 'other']) + expect(adapter.requests.map(request => request.provider)).toEqual(['mock', 'mock', 'other', 'other']) expect(agent.session.snapshotEvents().filter(event => event.type === 'llm/retry').map(event => ({ provider: event.data.provider, retry: event.data.retry, diff --git a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts index 28793ba945..17b4a558f8 100644 --- a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts +++ b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts @@ -48,8 +48,8 @@ async function harness( await ctx.plugin(Loader) if (packageService) await ctx.plugin(PluginPackages) ctx.loader.builtins.include = Include - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(AgentPresets, { default: 'fixture' }) await ctx.plugin(DeepSeekLlmApiExtensionRegistry) const inventory = enabled === undefined @@ -260,6 +260,7 @@ describe('DeepSeek plugin package inventory', () => { const ctx = new Context() contexts.push(ctx) await ctx.plugin(Loader) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(DeepSeekLlmApiExtensionRegistry) await ctx.plugin(PluginInventory) diff --git a/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts b/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts index ce1c4c96cb..04d0eac5e4 100644 --- a/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts +++ b/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts @@ -7,6 +7,7 @@ import { afterEach, expect, it } from 'vitest' import { Context, FiberState } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import AgentRegistry from '@deepseek-ai/dsh-agent' import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import * as PluginInventory from '../src/index.ts' @@ -40,6 +41,7 @@ it.each([true, false])('reports an unversioned package by name alongside a versi const siblingUrl = pathToFileURL(join(siblingDir, 'plugin.js')).href const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ + '- name: cordis:session-projections', '- name: cordis:agents', '- name: cordis:deepseek-extensions', '- name: cordis:plugin-inventory', @@ -53,6 +55,7 @@ it.each([true, false])('reports an unversioned package by name alongside a versi ctx.baseUrl = pathToFileURL(configPath).href await ctx.plugin(Loader) ctx.loader.builtins.include = Include + ctx.loader.builtins['session-projections'] = SessionProjectionRegistry ctx.loader.builtins.agents = AgentRegistry ctx.loader.builtins['deepseek-extensions'] = DeepSeekLlmApiExtensionRegistry ctx.loader.builtins['plugin-inventory'] = PluginInventory diff --git a/packages/shell/tool-bash/tests/tools.spec.ts b/packages/shell/tool-bash/tests/tools.spec.ts index 0b01d7c81b..791b52fa40 100644 --- a/packages/shell/tool-bash/tests/tools.spec.ts +++ b/packages/shell/tool-bash/tests/tools.spec.ts @@ -51,6 +51,7 @@ async function setup() { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalSubprocessRuntime) ;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir } @@ -66,6 +67,7 @@ async function setupWithJobs() { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) @@ -214,10 +216,10 @@ async function setupSandboxed(withApproval = false) { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) - await ctx.plugin(SessionProjectionRegistry) ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SandboxPolicyService, {}) await ctx.plugin(RecordingSandboxExecutor) @@ -507,6 +509,7 @@ describe('bash tool', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(LocalSubprocessRuntime) @@ -526,6 +529,7 @@ describe('the background surface follows the job registry', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalSubprocessRuntime) ;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir } @@ -637,6 +641,7 @@ describe('background execution through the job runtime', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) @@ -667,6 +672,7 @@ describe('background execution through the job runtime', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(CountingStartExecutor) @@ -1337,6 +1343,7 @@ describe('the model-facing bash tool builds its request from named args only (no provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) diff --git a/packages/shell/tool-pwsh/tests/tools.spec.ts b/packages/shell/tool-pwsh/tests/tools.spec.ts index 66920cc9e9..287ac0470c 100644 --- a/packages/shell/tool-pwsh/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh/tests/tools.spec.ts @@ -152,6 +152,7 @@ async function setup(toolConfig: Partial = {}, dshHome?: string provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(BashEnvPlugin, dshHome === undefined ? {} : { dshHome }) await ctx.plugin(FakeBash) @@ -166,6 +167,7 @@ async function setupWithJobs(toolConfig: Partial = {}, dshHome? provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) @@ -227,11 +229,11 @@ async function setupSandboxed(withApproval = false) { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) await ctx.plugin(BashEnvPlugin) - await ctx.plugin(SessionProjectionRegistry) // The loop's turnBoundary unit (the open-turn fold) is not mounted in this // bench — the loop itself is not composed. Register its open-turn fold so // the approval service's turn-enclosure gate reads the seeded log shape. @@ -618,6 +620,7 @@ describe('per-call sandbox policy resolution', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(BashEnvPlugin) await ctx.plugin(ConfiningFakeBash) @@ -914,6 +917,7 @@ describe('background execution through the job runtime', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(BashEnvPlugin) @@ -933,6 +937,7 @@ describe('the background surface follows the job registry', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(BashEnvPlugin) await ctx.plugin(FakeBash) diff --git a/packages/subagent/subagent/tests/external-activation-helpers.ts b/packages/subagent/subagent/tests/external-activation-helpers.ts index e31294e921..d83837df9e 100644 --- a/packages/subagent/subagent/tests/external-activation-helpers.ts +++ b/packages/subagent/subagent/tests/external-activation-helpers.ts @@ -19,7 +19,7 @@ function parentStore(ctx: Context): Promise> { onTestFinished(() => ctx.fiber.dispose()) const ready = (async () => { if (ctx.get('sessions') === undefined) await ctx.plugin(SessionStore) - await ctx.plugin(SessionProjectionRegistry) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) if (ctx.get('agents') === undefined) await ctx.plugin(AgentRegistry) return new WeakMap() })() diff --git a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts index 7375e52194..229c1e6eaf 100644 --- a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts +++ b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts @@ -449,6 +449,7 @@ describe('dsh-tool-workflow', () => { onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolTasks) @@ -659,10 +660,10 @@ describe('dsh-tool-workflow', () => { onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolTasks) - await ctx.plugin(SessionProjectionRegistry) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ diff --git a/packages/workflow/workflow-ptc/tests/host.spec.ts b/packages/workflow/workflow-ptc/tests/host.spec.ts index 4a866175a5..5254a25ceb 100644 --- a/packages/workflow/workflow-ptc/tests/host.spec.ts +++ b/packages/workflow/workflow-ptc/tests/host.spec.ts @@ -34,8 +34,8 @@ async function setup(execute?: (bindings: HostBindings, spec: PtcRunSpec) => Pro const ctx = new Context() onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjections) + await ctx.plugin(AgentRegistry) await ctx.plugin(SandboxPolicy, { mode: 'read-only' }) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) diff --git a/packages/workflow/workflow-ptc/tests/setup.ts b/packages/workflow/workflow-ptc/tests/setup.ts index 0a63dad5f8..3feb65d171 100644 --- a/packages/workflow/workflow-ptc/tests/setup.ts +++ b/packages/workflow/workflow-ptc/tests/setup.ts @@ -37,8 +37,8 @@ export async function mountWorkflowRuntime( options: { cwd?: string; mode?: SandboxMode; runtimeConfig?: NodeRuntimeConfig } = {}, ): Promise { if (!ctx.get('sessions')) await ctx.plugin(SessionStore) - if (!ctx.get('agents')) await ctx.plugin(AgentRegistry) if (!ctx.get('sessionProjections')) await ctx.plugin(SessionProjections) + if (!ctx.get('agents')) await ctx.plugin(AgentRegistry) if (!ctx.get('fs')) await ctx.plugin(FileSystem) if (!ctx.get('subprocess')) await ctx.plugin(Subprocess) if (!ctx.get('sandbox')) await ctx.plugin(Sandbox)