refactor(session-projection): clarify retained handle names

This commit is contained in:
_Kerman committed 2026-10-15 20:12:09 +08:00
1 parent d7a1aa839a
commit 491fa4a394
20 files changed
+63 -63

No files matched your search

+3 -3
View File
@@ -170,7 +170,7 @@ export async function inspectApiSession(
/** Owns every operation that may create, resume, or configure a Web Agent. */
export class ApiSessionAgentController {
private modelSelection!: ProjectionHandle<ModelSelectionProjectionState>
private modelSelectionProjectionHandle!: ProjectionHandle<ModelSelectionProjectionState>
private readonly resumes = new Map<SessionId, Promise<Agent>>()
private readonly claims = new Map<SessionId, Promise<void>>()
private readonly selections = new WeakMap<Agent, InstalledSelection>()
@@ -185,7 +185,7 @@ export class ApiSessionAgentController {
*/
async initialize(): Promise<void> {
const ctx = this.ctx
this.modelSelection = await ctx.sessionProjections.register(modelSelectionProjection)
this.modelSelectionProjectionHandle = await ctx.sessionProjections.register(modelSelectionProjection)
ctx.typert.lookups.configure('agent', async (sessionId: SessionId) => {
const found = await this.resolveAgent(sessionId)
if ('error' in found) throw found.error
@@ -300,7 +300,7 @@ export class ApiSessionAgentController {
selectionFor(agent: Agent): InstalledSelection {
const installed = this.selections.get(agent)
if (installed !== undefined) return installed
const projectionState = this.modelSelection.stateOf(agent.session)
const projectionState = this.modelSelectionProjectionHandle.stateOf(agent.session)
let picked = projectionState.pending === null
? undefined
: agentModelSelection(projectionState.pending)
@@ -127,7 +127,7 @@ const ADMITS_NOTHING: LegacyPrompt = { source: null, web: null }
* @returns after the projection reader and prompt registrations are ready.
*/
export async function apply(ctx: Context): Promise<void> {
const legacyPrompt = await ctx.sessionProjections.register({
const legacyPromptProjectionHandle = await ctx.sessionProjections.register({
key: LEGACY_PROMPT,
stateVersion: 1,
stateSchema: legacyPromptSchema,
@@ -136,7 +136,7 @@ export async function apply(ctx: Context): Promise<void> {
})
const admitted = (context: AssembleContext): LegacyPrompt => {
const session = context.agent?.session
return session === undefined ? ADMITS_NOTHING : legacyPrompt.stateOf(session)
return session === undefined ? ADMITS_NOTHING : legacyPromptProjectionHandle.stateOf(session)
}
ctx.systemPrompt.section({
name: HARNESS_SOURCE,
+2 -2
View File
@@ -155,7 +155,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
return created
}
const projection = await ctx.sessionProjections.register({
const timeContextProjectionHandle = await ctx.sessionProjections.register({
key: 'timeContext',
stateVersion: 2,
stateSchema: timeContextStateSchema,
@@ -190,7 +190,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
const decision = await next()
if (decision.kind === 'reject' || signal.aborted) return decision
const now = Date.now()
const state = projection.stateOf(agent.session)
const state = timeContextProjectionHandle.stateOf(agent.session)
if (refreshIntervalMs > 0) {
const lastInjection = state.lastInjectionTime
if (lastInjection != null
+4 -4
View File
@@ -308,7 +308,7 @@ export class AgentLoop extends Service implements AgentFactory {
private readonly ownership: FactoryOwnership
/** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */
private readonly runtime: { ctx: Context }
private inbox!: ProjectionHandle<InboxState>
private inboxProjectionHandle!: ProjectionHandle<InboxState>
constructor(ctx: Context, config: Config) {
super(ctx, 'agentLoop')
@@ -327,7 +327,7 @@ export class AgentLoop extends Service implements AgentFactory {
const ownership = this.ownership
// The inbox remains readable until agent teardown finishes.
ctx.effect(() => () => ownership.dispose(), 'agentLoop.transactions()')
this.inbox = await ctx.sessionProjections.register(inboxProjectionDefinition)
this.inboxProjectionHandle = await ctx.sessionProjections.register(inboxProjectionDefinition)
ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()')
ctx.systemPrompt.variable('provider', context => context.agent?.options.provider)
ctx.systemPrompt.variable('model', context => context.agent?.options.model)
@@ -543,10 +543,10 @@ export class AgentLoop extends Service implements AgentFactory {
}
}
const untrack = this.ownership.track(dispose)
const inbox = this.inbox
const inboxProjectionHandle = this.inboxProjectionHandle
try {
unfollowOwner = ownerCtx.effect(function* () {
const created = machine = new ReactLoopAgent(loopCtx, id, options, session, () => inbox.stateOf(session), lastTurn)
const created = machine = new ReactLoopAgent(loopCtx, id, options, session, () => inboxProjectionHandle.stateOf(session), lastTurn)
machineReady.resolve()
// Adopt the scope on its original fiber so it cannot unload before teardown.
// A loop already unloading during minting owns the provisional scope itself.
+3 -3
View File
@@ -247,7 +247,7 @@ interface FactorySlot {
export class AgentRegistry extends Service {
static inject = ['sessionProjections']
private turnBoundaryProjection!: ProjectionHandle<TurnBoundaryProjection>
private turnBoundaryProjectionHandle!: ProjectionHandle<TurnBoundaryProjection>
private store = new Map<SessionId, AgentEntry>()
private factory: FactorySlot | undefined
private readonly initiators = new AsyncLocalStorage<Agent | undefined>()
@@ -290,7 +290,7 @@ export class AgentRegistry extends Service {
}
protected async [Service.init](): Promise<void> {
this.turnBoundaryProjection = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
this.turnBoundaryProjectionHandle = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition)
}
/**
@@ -299,7 +299,7 @@ export class AgentRegistry extends Service {
* @returns the current turn and step positions; do not mutate the returned value.
*/
turnBoundaryOf(session: Session): TurnBoundaryProjection {
return this.turnBoundaryProjection.stateOf(session)
return this.turnBoundaryProjectionHandle.stateOf(session)
}
/**
@@ -90,8 +90,8 @@ export class TeamService extends Service {
protected async [Service.init](): Promise<void> {
const ctx = this.ctx
const projection = await ctx.sessionProjections.register(teamProjectionDefinition)
this.journal = new TeamJournal(ctx, projection, (root) => { this.activity.notify(TeamId(root.id)) })
const teamProjectionHandle = await ctx.sessionProjections.register(teamProjectionDefinition)
this.journal = new TeamJournal(ctx, teamProjectionHandle, (root) => { this.activity.notify(TeamId(root.id)) })
this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers)
this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks)
ctx.effect(() => () => this.disposeRuntime(), 'agentTeams.runtimeLifecycle()')
@@ -15,12 +15,12 @@ export class TeamJournal {
/**
* @param ctx - Team service context with the injected Session service.
* @param projection - Team state registration owned by the Team service.
* @param teamProjectionHandle - Team state registration owned by the Team service.
* @param onCommit - synchronous notification after the Team event flush succeeds.
*/
constructor(
private readonly ctx: Context,
private readonly projection: ProjectionHandle<TeamProjectionState>,
private readonly teamProjectionHandle: ProjectionHandle<TeamProjectionState>,
private readonly onCommit: (root: Agent) => void,
) {}
@@ -31,7 +31,7 @@ export class TeamJournal {
* @throws when the durable Team projection has failed.
*/
state(root: Agent): TeamState {
const projection = this.projection.stateOf(root.session)
const projection = this.teamProjectionHandle.stateOf(root.session)
if (projection.failure !== undefined) throw new Error(projection.failure)
return projection
}
+4 -4
View File
@@ -247,7 +247,7 @@ export class GoalService extends TypertRemoteService {
private readonly resolved: ResolvedConfig
private readonly runtimeStates = new WeakMap<Session, GoalRuntimeState>()
private projection!: ProjectionHandle<GoalProjectionState>
private goalProjectionHandle!: ProjectionHandle<GoalProjectionState>
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'goals')
@@ -258,7 +258,7 @@ export class GoalService extends TypertRemoteService {
protected async [Service.init](): Promise<void> {
const ctx = this.ctx
this.projection = await ctx.sessionProjections.register(goalProjectionDefinition)
this.goalProjectionHandle = await ctx.sessionProjections.register(goalProjectionDefinition)
ctx.on('agent/created', ({ agent }) => {
this.setActivation(agent.session, 'disarmed')
})
@@ -480,7 +480,7 @@ export class GoalService extends TypertRemoteService {
/** Read the current durable projection maintained by the registry. */
private state(session: Session): GoalProjection | null {
const state = this.projection.stateOf(session)
const state = this.goalProjectionHandle.stateOf(session)
if (state.failure !== null) throw new Error(state.failure)
return state.current
}
@@ -502,7 +502,7 @@ export class GoalService extends TypertRemoteService {
const runtime = this.runtimeState(session)
if (runtime.activation === activation) return
runtime.activation = activation
const state = this.projection.stateOf(session)
const state = this.goalProjectionHandle.stateOf(session)
if (state.failure !== null) return
const goal = this.view(state.current, runtime)
this.ctx.emit('goal/activation-changed', {
@@ -204,7 +204,7 @@ export class PermissionPresetService extends TypertRemoteService {
private autoAdmit: (() => void) | undefined
private defaultSettings: () => PermissionSettings
private projection!: ProjectionHandle<PermissionProjectionState>
private permissionProjectionHandle!: ProjectionHandle<PermissionProjectionState>
constructor(ctx: Context, config: Config) {
super(ctx, 'permissionPresets')
@@ -239,7 +239,7 @@ export class PermissionPresetService extends TypertRemoteService {
const selectionSchema = zod.object({
currentValue: zod.string().min(1),
}) as zod.ZodType<PermissionSelection>
this.projection = await ctx.sessionProjections.register({
this.permissionProjectionHandle = await ctx.sessionProjections.register({
key: 'permissions',
stateVersion: 2,
stateSchema: permissionStateSchema,
@@ -331,7 +331,7 @@ export class PermissionPresetService extends TypertRemoteService {
}
private permissionState(session: Session): PermissionProjectionState {
return this.projection.stateOf(session)
return this.permissionProjectionHandle.stateOf(session)
}
/**
@@ -78,7 +78,7 @@ type QueuedReply = { messageId: MessageId; claimedTurn?: number }
/** `ctx.userQuestions`: validation plus the scoped answerer waterfall. */
export class UserQuestionService extends TypertRemoteService {
static Config = z.object({})
private projection: ProjectionHandle<UserQuestionProjectionState> | undefined
private userQuestionProjectionHandle: ProjectionHandle<UserQuestionProjectionState> | undefined
private readonly waits = new Map<Agent, Map<ToolCallId, TimedQuestionWait>>()
private readonly queuedReplies = new WeakMap<Session, Map<ToolCallId, QueuedReply>>()
@@ -128,8 +128,8 @@ export class UserQuestionService extends TypertRemoteService {
protected async [Service.init](): Promise<void> {
const binding = this.ctx.inject(['sessionProjections'], async (ctx) => {
this.projection = await ctx.sessionProjections.register(userQuestionProjectionDefinition)
return () => { this.projection = undefined }
this.userQuestionProjectionHandle = await ctx.sessionProjections.register(userQuestionProjectionDefinition)
return () => { this.userQuestionProjectionHandle = undefined }
})
if (this.ctx.get('sessionProjections') !== undefined) await binding
}
@@ -150,7 +150,7 @@ export class UserQuestionService extends TypertRemoteService {
}
private continued(agent: Agent): readonly PendingUserQuestion[] {
return this.projection?.stateOf(agent.session).questions.active.filter(question => question.state === 'continued') ?? []
return this.userQuestionProjectionHandle?.stateOf(agent.session).questions.active.filter(question => question.state === 'continued') ?? []
}
/**
+2 -2
View File
@@ -123,7 +123,7 @@ declare module '@deepseek-ai/dsh-session-projection/types' {
*/
export async function apply(ctx: Context, config: Config = {}, internals: RetryInternals = {}): Promise<void> {
validateConfig(config)
const projection = await ctx.sessionProjections.register({
const llmRetryProjectionHandle = await ctx.sessionProjections.register({
key: 'llmRetry',
stateVersion: 1,
stateSchema: llmRetryStateSchema,
@@ -218,7 +218,7 @@ export async function apply(ctx: Context, config: Config = {}, internals: RetryI
}
const policyKey = retryPolicyKey(policy)
const retryState = projection.stateOf(agent.session)
const retryState = llmRetryProjectionHandle.stateOf(agent.session)
const previous = retryState[retryStateKey(provider, policyKey)]
const previousRetry = previous?.retry ?? 0
if (policy.mode === 'normal' && previousRetry >= policy.maxRetries) return next()
+3 -3
View File
@@ -177,7 +177,7 @@ export class PlanModeController extends Service {
/** Validated deployment-owned guidance. */
private readonly section: string
private projection!: ProjectionHandle<PlanUnitState>
private planProjectionHandle!: ProjectionHandle<PlanUnitState>
/**
* Latest selection per session awaiting the next accepted in-turn pre-step.
@@ -193,7 +193,7 @@ export class PlanModeController extends Service {
protected async [Service.init](): Promise<void> {
const ctx = this.ctx
this.projection = await ctx.sessionProjections.register(planProjectionDefinition)
this.planProjectionHandle = await ctx.sessionProjections.register(planProjectionDefinition)
let disposed = false
// Pre-step is outside Session.append publication, so it can append the
// log-only mode event inside an open turn without re-entering the session.
@@ -389,7 +389,7 @@ export class PlanModeController extends Service {
}
private planState(session: Session): PlanUnitState {
return this.projection.stateOf(session)
return this.planProjectionHandle.stateOf(session)
}
/**
@@ -57,7 +57,7 @@ export class AgentPresetRegistry extends TypertRemoteService {
default: z.string().required(),
selectedDefault: z.string().volatile(),
})
private agentPreset!: ProjectionHandle<string | null>
private agentPresetProjectionHandle!: ProjectionHandle<string | null>
private readonly owner: Context
private readonly definitions = new Map<string, Definition>()
private readonly generations = new Map<ScopeKey, Generation>()
@@ -71,7 +71,7 @@ export class AgentPresetRegistry extends TypertRemoteService {
protected async [Service.init](): Promise<void> {
const ctx = this.owner
this.agentPreset = await ctx.sessionProjections.register(agentPresetProjectionDefinition)
this.agentPresetProjectionHandle = await ctx.sessionProjections.register(agentPresetProjectionDefinition)
ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) })
ctx.on('session/event', (session, event) => {
if (event.type === 'agent-preset/selected') ctx.emit('agent-preset/selected', session.id, event.data.agentPreset)
@@ -87,7 +87,7 @@ export class AgentPresetRegistry extends TypertRemoteService {
* @returns the recorded preset, or null when none was selected.
*/
presetForSession(session: Session): string | null {
return this.agentPreset.stateOf(session)
return this.agentPresetProjectionHandle.stateOf(session)
}
/** Register and eagerly load a definition; activation failure remains visible in the roster.
@@ -370,7 +370,7 @@ export class AgentPresetRegistry extends TypertRemoteService {
}
const current = this.definitions.get(agentPreset)?.generation
if (current !== undefined && this.generationFor(agent.ctx) === current
&& this.agentPreset.stateOf(agent.session) === agentPreset) return agentPreset
&& this.agentPresetProjectionHandle.stateOf(agent.session) === agentPreset) return agentPreset
const preset = await this.recompose(agent.ctx, agentPreset)
agent.session.append('agent-preset/selected', { agentPreset: preset.id })
return preset.id
+3 -3
View File
@@ -122,7 +122,7 @@ export class SandboxPolicyService extends Service {
readonly defaultMode: SandboxMode
/** The absolute `workspace-write` fallback root for calls without a session cwd. */
readonly workspaceRoot: string
private projection!: ProjectionHandle<SandboxMode | null>
private sandboxModeProjectionHandle!: ProjectionHandle<SandboxMode | null>
constructor(ctx: Context, config: Config) {
super(ctx, 'sandboxPolicy')
@@ -135,7 +135,7 @@ export class SandboxPolicyService extends Service {
protected async [Service.init](): Promise<void> {
const ctx = this.ctx
this.projection = await ctx.sessionProjections.register({
this.sandboxModeProjectionHandle = await ctx.sessionProjections.register({
key: 'sandboxMode',
stateVersion: 1,
stateSchema: sandboxModeStateSchema,
@@ -181,7 +181,7 @@ export class SandboxPolicyService extends Service {
* @returns the last logged mode, or `undefined` without one.
*/
overrideOf(session: Session): SandboxMode | undefined {
return this.projection.stateOf(session) ?? undefined
return this.sandboxModeProjectionHandle.stateOf(session) ?? undefined
}
}
@@ -58,8 +58,8 @@ const definition = {
`await register(definition)` installs the unit and returns a handle with synchronous `stateOf(session)` reads and `dispose(): Promise<void>`. Await `dispose()` to release the registration; repeated calls are safe. Registrants with the same key and `stateVersion` share cells; incompatible or invalid versions reject registration. Registration belongs to the calling fiber. The last unregister removes the unit from global event drives, snapshots, checkpoints, and change notifications. Existing handles remain readable: each retains its original unit and folds new Session events on demand, even if another version later registers the same key. Reading a handle does not initialize unrelated units or register it again. Carriers use `snapshot(session)` for a consistent client-view cut and `onChanged(listener)` for changes to registered units.
```text
const projection = await ctx.sessionProjections.register(definition)
const state = projection.stateOf(session)
const todoProjectionHandle = await ctx.sessionProjections.register(definition)
const state = todoProjectionHandle.stateOf(session)
const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
```
@@ -58,8 +58,8 @@ const definition = {
`await register(definition)` 安装单元,返回提供同步 `stateOf(session)` 读取和 `dispose(): Promise<void>` 的 handle。等待 `dispose()` 完成即可释放注册;重复调用是安全的。具有相同 key 和 `stateVersion` 的注册方共享 cell;版本不兼容或非法时注册会拒绝。注册归调用方 fiber 所有。最后一个注册方注销后,单元退出全局事件驱动、快照、检查点和变更通知。已有 handle 仍可读取:它保留原有单元,按需折叠新增的 Session 事件,即使同一 key 后来注册了其他版本也不受影响。读取 handle 不会初始化无关单元,也不会重新注册。载体用 `snapshot(session)` 读取一致的客户端视图切面,用 `onChanged(listener)` 订阅已注册单元的变更。
```text
const projection = await ctx.sessionProjections.register(definition)
const state = projection.stateOf(session)
const todoProjectionHandle = await ctx.sessionProjections.register(definition)
const state = todoProjectionHandle.stateOf(session)
const { asOfSeq, values } = ctx.sessionProjections.snapshot(session)
```
+3 -3
View File
@@ -306,7 +306,7 @@ export class SessionTitleService extends Service {
maxTitleBytes: z.number().step(1).min(1).required(),
})
private titleInput!: ProjectionHandle<TitleInputState>
private titleInputProjectionHandle!: ProjectionHandle<TitleInputState>
private readonly config: ResolvedConfig
private readonly ownerFiber: Fiber
private registration: ProviderRegistration | undefined
@@ -347,7 +347,7 @@ export class SessionTitleService extends Service {
await ctx.sessionProjections.register(titleProjectionDefinition)
this.titleInput = await ctx.sessionProjections.register<'titleInput', TitleInputState>({
this.titleInputProjectionHandle = await ctx.sessionProjections.register<'titleInput', TitleInputState>({
key: 'titleInput',
stateVersion: 3,
stateSchema: titleInputStateSchema,
@@ -734,7 +734,7 @@ export class SessionTitleService extends Service {
}
private titleInputOf(session: Session): TitleInputState {
return this.titleInput.stateOf(session)
return this.titleInputProjectionHandle.stateOf(session)
}
/** Queue detached service work and retain it through service disposal. */
@@ -72,7 +72,7 @@ export class WorkingDirectoryService extends Service {
private readonly operations = new Set<Promise<unknown>>()
private readonly lifecycle = new AbortController()
private projection!: ProjectionHandle<string | null>
private workingDirectoryProjectionHandle!: ProjectionHandle<string | null>
constructor(ctx: Context, config: Config) {
super(ctx, 'workingDirectory')
@@ -84,7 +84,7 @@ export class WorkingDirectoryService extends Service {
protected async [Service.init](): Promise<void> {
const ctx = this.ctx
const directorySchema = zod.string().min(1).refine(isAbsolute).nullable()
this.projection = await ctx.sessionProjections.register({
this.workingDirectoryProjectionHandle = await ctx.sessionProjections.register({
key: 'workingDirectory',
stateVersion: 1,
stateSchema: directorySchema,
@@ -125,7 +125,7 @@ export class WorkingDirectoryService extends Service {
* @returns its effective absolute directory.
*/
get(session: Session): string {
return this.projection.stateOf(session) ?? this.defaultDirectory
return this.workingDirectoryProjectionHandle.stateOf(session) ?? this.defaultDirectory
}
/**
@@ -144,7 +144,7 @@ export class WorkingDirectoryService extends Service {
control.throwIfAborted()
agent.ctx.fiber.assertActive()
if (info?.type === 'directory') {
if (this.projection.stateOf(agent.session) === null) {
if (this.workingDirectoryProjectionHandle.stateOf(agent.session) === null) {
agent.session.append('working-directory/change', { cwd: current })
}
return current
+4 -4
View File
@@ -160,7 +160,7 @@ export async function apply(ctx: Context, config: Config, session?: Session): Pr
const toolName = config.toolName ?? 'subagent'
const modelSelectionCapable = config.modelSelectionSettings === true
const modelSelectionProjection = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
const subagentModelSelectionProjectionHandle = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => {
presets.assertProvider(subagentProvider)
@@ -416,7 +416,7 @@ export async function apply(ctx: Context, config: Config, session?: Session): Pr
const freshSession = target.firstLiveSeq === 0
// oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
&& target.eventAt(SessionSeq(0))?.type !== 'session/end-seed'
let allowedModels = subagentModelSelectionPolicy(modelSelectionProjection, target)
let allowedModels = subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, target)
if (allowedModels === undefined) {
const parentId = target.header.origin === 'subagent'
? target.header.parentSession
@@ -429,14 +429,14 @@ export async function apply(ctx: Context, config: Config, session?: Session): Pr
const parent = sessions.get(parentId)
allowedModels = parent === undefined
? undefined
: subagentModelSelectionPolicy(modelSelectionProjection, parent)
: subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, parent)
} else if (freshSession) {
const current = settings.current()
allowedModels = current.enabled ? current.allowedModels : undefined
}
}
if (allowedModels !== undefined) {
recordSubagentModelSelection(modelSelectionProjection, target, allowedModels)
recordSubagentModelSelection(subagentModelSelectionProjectionHandle, target, allowedModels)
}
return allowedModels === undefined ? undefined : { routes: allowedModels }
}
@@ -52,29 +52,29 @@ export const subagentModelSelectionProjectionDefinition = {
/**
* Read the exact route list captured for a model-selectable definition.
* @param projection - registered policy state reader.
* @param subagentModelSelectionProjectionHandle - registered policy state reader.
* @param session - session whose durable decision is read.
* @returns a detached route list, or undefined for the fixed-route definition.
*/
export function subagentModelSelectionPolicy(
projection: Pick<ProjectionHandle<AllowedModelRoute[] | null>, 'stateOf'>,
subagentModelSelectionProjectionHandle: Pick<ProjectionHandle<AllowedModelRoute[] | null>, 'stateOf'>,
session: Session,
): AllowedModelRoute[] | undefined {
return projection.stateOf(session)?.map(route => ({ ...route }))
return subagentModelSelectionProjectionHandle.stateOf(session)?.map(route => ({ ...route }))
}
/**
* Append the route policy once, before its definition can reach a model request.
* @param projection - registered policy state reader.
* @param subagentModelSelectionProjectionHandle - registered policy state reader.
* @param session - session receiving the model-selectable definition.
* @param allowedModels - exact routes the definition may select explicitly.
*/
export function recordSubagentModelSelection(
projection: Pick<ProjectionHandle<AllowedModelRoute[] | null>, 'stateOf'>,
subagentModelSelectionProjectionHandle: Pick<ProjectionHandle<AllowedModelRoute[] | null>, 'stateOf'>,
session: Session,
allowedModels: readonly AllowedModelRoute[],
): void {
if (subagentModelSelectionPolicy(projection, session) !== undefined) return
if (subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, session) !== undefined) return
session.append('subagent/model-selection-policy', {
allowedModels: allowedModels.map(route => ({ ...route })),
})