c58cc23d45
A preset publishes its services behind `isolate` realms, which is what makes them per session — and what makes them invisible to every host context. The api-proxy kept reading the root realm, so requests that are ABOUT a session but arrive from outside it answered for a singleton that no longer exists: `goal.pause`/`clear` and `skill.list` returned "this deployment does not mount @deepseek-ai/dsh-goal / dsh-skill" for sessions whose composition mounts exactly that. Verified against a running host before and after. `agentPresets.serviceFor(agent, name)` addresses the instance instead, reading the same subtree-ownership relation `leakedServices` already uses, inverted. It is read addressing for a caller holding the agent: a host row that `inject`s a service cannot use it, because injection resolves before any session exists — which is why `tools` and `subagents` stay host-plane and this is not a way around that. Tool presenters had the same shape and the same cure: `viewFor` looked definitions up without a scope while the global layer is empty by design, so every card degraded to the generic renderer. It now takes the owning agent. Cold resume through `agentFor()` mounted no preset at all, so every generic entry point — prompt, models, commands — rebuilt a restarted session on host tools and the deployment persona. It composes the recorded preset now, as the other resume path already did.
235 lines
9.4 KiB
TypeScript
235 lines
9.4 KiB
TypeScript
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
import { Context } from 'cordis'
|
|
import Loader from '@cordisjs/plugin-loader'
|
|
import Include from '@cordisjs/plugin-include'
|
|
import LlmService from '@deepseek-ai/dsh-llm'
|
|
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
|
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
|
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
|
import AgentRegistry, { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
|
|
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
import AgentPresets, { leakedServices, livePresetMounts } from '@deepseek-ai/dsh-agent-presets'
|
|
|
|
declare module 'cordis' {
|
|
interface Context {
|
|
/** Published by the `isolated` fixture preset behind an entry-local realm. */
|
|
fixtureIsolatedSvc: { label: string }
|
|
}
|
|
}
|
|
|
|
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures')
|
|
const ROOTS = [
|
|
{ path: join(FIXTURES, 'system'), trust: 'system' as const },
|
|
{ path: join(FIXTURES, 'user'), trust: 'user' as const },
|
|
]
|
|
|
|
/** A composition carrying the registries a preset contributes to, plus the preset roster. */
|
|
async function harness(): Promise<Context> {
|
|
const ctx = new Context()
|
|
ctx.baseUrl = pathToFileURL(FIXTURES).href + '/'
|
|
await ctx.plugin(Loader)
|
|
ctx.loader.builtins.include = Include
|
|
await ctx.plugin(LlmService)
|
|
await ctx.plugin(SessionStore)
|
|
await ctx.plugin(SystemPrompt, { persona: '' })
|
|
await ctx.plugin(ToolRegistry)
|
|
await ctx.plugin(AgentRegistry)
|
|
await ctx.plugin(AgentLoop, { agents: [] })
|
|
await ctx.plugin(AgentPresets, { default: 'standard', roots: ROOTS })
|
|
return ctx
|
|
}
|
|
|
|
/** Create one agent composed from `presetId`, exactly as a factory `setup` would. */
|
|
async function agentOn(ctx: Context, id: string, presetId?: string): Promise<Agent> {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId(id),
|
|
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, presetId),
|
|
})
|
|
return handle.agent
|
|
}
|
|
|
|
const toolNames = (ctx: Context, agent?: Agent): string[] =>
|
|
ctx.tools.schemas(agent).map(schema => schema.name).sort()
|
|
|
|
/** Every service registration in the runtime, regardless of which realm holds it. */
|
|
function providedServiceNames(ctx: Context): string[] {
|
|
const store = ctx.reflect.store
|
|
return Object.getOwnPropertySymbols(store)
|
|
.map(key => store[key]?.name)
|
|
.filter((name): name is string => name !== undefined)
|
|
}
|
|
|
|
/** Whether the root realm maps `name` to a live registration. */
|
|
function rootResolves(ctx: Context, name: string): boolean {
|
|
const key = ctx.root[Context.isolate][name]
|
|
return key !== undefined && ctx.reflect.store[key] !== undefined
|
|
}
|
|
|
|
let ctx: Context
|
|
beforeEach(async () => {
|
|
ctx = await harness()
|
|
})
|
|
|
|
describe('composing an agent from a preset', () => {
|
|
it('gives each session only its own preset\'s tools', async () => {
|
|
const alpha = await agentOn(ctx, 'sess-alpha', 'standard')
|
|
const beta = await agentOn(ctx, 'sess-beta', 'minimal')
|
|
|
|
expect(toolNames(ctx, alpha)).toEqual(['alpha'])
|
|
expect(toolNames(ctx, beta)).toEqual(['beta'])
|
|
expect(toolNames(ctx)).toEqual([])
|
|
})
|
|
|
|
it('scopes prompt sections and assembled schemas to the same session', async () => {
|
|
const alpha = await agentOn(ctx, 'sess-alpha', 'standard')
|
|
const beta = await agentOn(ctx, 'sess-beta', 'minimal')
|
|
|
|
const alphaPrompt = await ctx.systemPrompt.assemble(assembleContextFor(alpha))
|
|
const betaPrompt = await ctx.systemPrompt.assemble(assembleContextFor(beta))
|
|
|
|
expect(alphaPrompt.sections.map(section => section.name)).toContain('preset:alpha')
|
|
expect(alphaPrompt.sections.map(section => section.name)).not.toContain('preset:beta')
|
|
expect(betaPrompt.sections.map(section => section.name)).toContain('preset:beta')
|
|
expect(alphaPrompt.tools.map(schema => schema.name)).toEqual(['alpha'])
|
|
})
|
|
|
|
it('mounts the default preset when the caller names none', async () => {
|
|
const agent = await agentOn(ctx, 'sess-default')
|
|
|
|
expect(toolNames(ctx, agent)).toEqual(['alpha'])
|
|
})
|
|
|
|
it('lets two sessions share one preset without colliding', async () => {
|
|
const first = await agentOn(ctx, 'sess-first', 'standard')
|
|
const second = await agentOn(ctx, 'sess-second', 'standard')
|
|
|
|
expect(toolNames(ctx, first)).toEqual(['alpha'])
|
|
expect(toolNames(ctx, second)).toEqual(['alpha'])
|
|
})
|
|
|
|
it('unwinds one session\'s composition without touching another\'s', async () => {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('sess-gone'),
|
|
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'standard'),
|
|
})
|
|
const survivor = await agentOn(ctx, 'sess-stays', 'minimal')
|
|
expect(toolNames(ctx, handle.agent)).toEqual(['alpha'])
|
|
|
|
await handle.dispose()
|
|
|
|
expect(ctx.agents.get(SessionId('sess-gone'))).toBeUndefined()
|
|
expect(toolNames(ctx, survivor)).toEqual(['beta'])
|
|
expect(toolNames(ctx)).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('rejecting a composition that cannot be used', () => {
|
|
it('refuses to mount into a context that carries no agent scope', async () => {
|
|
await expect(ctx.agentPresets.mount(ctx, 'standard'))
|
|
.rejects.toThrow(/unscoped context/)
|
|
})
|
|
|
|
it('rolls the whole agent back when a row fails to load', async () => {
|
|
await expect(agentOn(ctx, 'sess-broken', 'broken')).rejects.toThrow(/failed to mount/)
|
|
|
|
expect(ctx.agents.get(SessionId('sess-broken'))).toBeUndefined()
|
|
expect(toolNames(ctx)).toEqual([])
|
|
})
|
|
|
|
it('names the unresolved service when a row never activates', async () => {
|
|
await expect(agentOn(ctx, 'sess-pending', 'pending'))
|
|
.rejects.toThrow(/waiting for serviceThatDoesNotExist/)
|
|
})
|
|
|
|
it('rejects a row that publishes a process-global service', async () => {
|
|
await expect(agentOn(ctx, 'sess-leaky', 'leaky'))
|
|
.rejects.toThrow(/process-global service\(s\) \[aaaFixtureLeakedSvc, zzzFixtureLeakedSvc\]/)
|
|
|
|
// The rejected subtree is fully unwound, so its registrations are gone from
|
|
// the store rather than merely unreachable.
|
|
expect(providedServiceNames(ctx)).not.toContain('aaaFixtureLeakedSvc')
|
|
expect(providedServiceNames(ctx)).not.toContain('zzzFixtureLeakedSvc')
|
|
})
|
|
|
|
it('accepts the same provider behind an isolate realm', async () => {
|
|
const agent = await agentOn(ctx, 'sess-isolated', 'isolated')
|
|
|
|
expect(agent.id).toBe(SessionId('sess-isolated'))
|
|
// The provider ran, but under a realm-private symbol the root cannot reach.
|
|
expect(providedServiceNames(ctx)).toContain('fixtureIsolatedSvc')
|
|
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
|
|
})
|
|
|
|
it('addresses one agent\'s instance of a realm-private service', async () => {
|
|
const first = await agentOn(ctx, 'sess-reach-a', 'isolated')
|
|
const second = await agentOn(ctx, 'sess-reach-b', 'isolated')
|
|
|
|
// The realm keeps the service out of every host context — that is what
|
|
// makes it per session — so a caller holding the agent is the only way a
|
|
// request from OUTSIDE the session can read the instance it is about.
|
|
expect(rootResolves(ctx, 'fixtureIsolatedSvc')).toBe(false)
|
|
const mine = ctx.agentPresets.serviceFor(first, 'fixtureIsolatedSvc')
|
|
const theirs = ctx.agentPresets.serviceFor(second, 'fixtureIsolatedSvc')
|
|
expect(mine).toBeDefined()
|
|
expect(theirs).toBeDefined()
|
|
// Each agent gets ITS own: the addressing is per subtree, not a lookup
|
|
// that happens to find the first match.
|
|
expect(mine).not.toBe(theirs)
|
|
})
|
|
|
|
it('answers undefined for a service the agent\'s preset does not mount', async () => {
|
|
const agent = await agentOn(ctx, 'sess-reach-none', 'standard')
|
|
|
|
expect(ctx.agentPresets.serviceFor(agent, 'fixtureIsolatedSvc')).toBeUndefined()
|
|
})
|
|
|
|
it('reports the known ids when a preset is unknown', async () => {
|
|
await expect(ctx.agentPresets.resolve('nope'))
|
|
.rejects.toThrow(/preset "nope" not found \(available: .*standard/)
|
|
})
|
|
})
|
|
|
|
describe('the preset roster', () => {
|
|
it('lists every root\'s presets with the earlier root winning', async () => {
|
|
const listed = await ctx.agentPresets.list()
|
|
|
|
expect(listed.map(preset => preset.id).sort())
|
|
.toEqual(['broken', 'isolated', 'late', 'leaky', 'minimal', 'pending', 'standard'])
|
|
expect(listed.find(preset => preset.id === 'standard')?.trust).toBe('system')
|
|
})
|
|
|
|
it('exposes the configured default id', () => {
|
|
expect(ctx.agentPresets.defaultId).toBe('standard')
|
|
})
|
|
})
|
|
|
|
describe('a roster with nothing in it', () => {
|
|
it('says so instead of naming an empty list of candidates', async () => {
|
|
const bare = new Context()
|
|
await bare.plugin(Loader)
|
|
await bare.plugin(AgentPresets, { default: 'standard', roots: [] })
|
|
|
|
await expect(bare.agentPresets.resolve())
|
|
.rejects.toThrow(/preset "standard" not found \(available: none\)/)
|
|
})
|
|
})
|
|
|
|
describe('attributing a service to a subtree', () => {
|
|
it('attributes nothing to a subtree that is already torn down', async () => {
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('sess-torn'),
|
|
setup: async (agentCtx: Context) => void await ctx.agentPresets.mount(agentCtx, 'standard'),
|
|
})
|
|
const [mount] = livePresetMounts().filter(entry => entry.presetId === 'standard')
|
|
expect(mount).toBeDefined()
|
|
|
|
await handle.dispose()
|
|
|
|
// A disposed subtree owns nothing, so it can never be blamed for a service
|
|
// some other subtree published under the same name afterwards.
|
|
expect(leakedServices(ctx, mount!.fiber)).toEqual([])
|
|
})
|
|
})
|