返回源码地图

packages/preset/agent-preset-registry/src/mount.ts

main snapshot · da00f7f5358f · 正文引用章节 04 / 04 / 04;完整原文可核对,不声称全文件人工逐行审计

完整原文供逐行核对;页面收录不代表每行都经过人工语义审核。MIT 许可见 许可证。

1/** Runtime plugin trees shared by Agents selecting one preset revision. */
2import { Context, type Fiber } from '@deepseek-ai/cordis'
3import { EntryTree, type EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
4import { prepareProfileEntries } from '@deepseek-ai/dsh-app-boot'
5import type { PresetDefinition } from './definition.ts'
6import { scopeOf, type ScopeKey } from '@deepseek-ai/dsh-scope'
7
8/** In-memory Loader tree; only the profile configuration editor persists definitions. */
9class PresetTree extends EntryTree {
10 constructor(ctx: Context) {
11 const owner = ctx.fiber.entry
12 const subtree = owner?.subtree
13 const subgroup = owner?.subgroup
14 super(ctx)
15 if (owner !== undefined) {
16 if (subtree === undefined) delete owner.subtree
17 else owner.subtree = subtree
18 if (subgroup === undefined) delete owner.subgroup
19 else owner.subgroup = subgroup
20 }
21 }
22
23 override write(): void {}
24}
25
26/** One live revision shared by Agents and scoped readers. */
27export interface PresetMount {
28 /** The preset the subtree was composed from. */
29 readonly presetId: string
30 /** The mounted subtree's fiber. */
31 readonly fiber: Fiber
32 /** Loader entry tree whose active rows form this standing composition. */
33 readonly tree: EntryTree
34 /** The standing scope key agents are parented to (undefined only in torn-down records). */
35 readonly key: ScopeKey | undefined
36}
37
38/**
39 * Whether `fiber` is `root` itself or is mounted anywhere inside its subtree.
40 *
41 * Membership is object identity. `uid` looks like a cheaper key but is a
42 * per-registry counter, so fibers in two different roots collide on it and a
43 * subtree in one runtime would be blamed for a service published in another.
44 * @param fiber - the fiber to locate.
45 * @param root - the subtree root to test membership against.
46 * @returns true when `fiber` belongs to `root`'s subtree.
47 */
48function withinFiber(fiber: Fiber, root: Fiber): boolean {
49 let current = fiber
50 while (true) {
51 if (current === root) return true
52 const parent = current.parent.fiber
53 if (parent === current) return false
54 current = parent
55 }
56}
57
58/**
59 * Service names the mounted subtree published into the root realm.
60 *
61 * A provider without an `isolate` realm stores its implementation under the
62 * root's symbol for that name, which is exactly the comparison below; a
63 * provider inside an `isolate` realm stores under a realm-private symbol and
64 * is correctly absent here.
65 * @param ctx - any context of the runtime whose service store is inspected.
66 * @param mount - the mounted subtree's fiber.
67 * @returns the leaked service names in lexical order.
68 */
69export function leakedServices(ctx: Context, mount: Fiber): string[] {
70 const store = ctx.reflect.store
71 const rootIsolate = ctx.root[Context.isolate]
72 const leaked: string[] = []
73 for (const key of Object.getOwnPropertySymbols(store)) {
74 const impl = store[key]
75 /* v8 ignore next -- cordis deletes a store slot on disposal rather than
76 clearing it, so an own symbol always resolves; the guard exists only
77 because the store's index signature is optional. */
78 if (impl === undefined) continue
79 if (!withinFiber(impl.fiber, mount)) continue
80 if (rootIsolate[impl.name] === key) leaked.push(impl.name)
81 }
82 return leaked.sort((left, right) => left.localeCompare(right))
83}
84
85/**
86 * Read a service implementation owned by one retained preset, including isolated realms.
87 *
88 * Ownership is the same relation {@link leakedServices} reads, inverted: there
89 * it names implementations a subtree published into the ROOT realm, here it
90 * names the one this subtree published anywhere. Fiber membership is object
91 * identity for the reason stated on {@link withinFiber}.
92 *
93 * @param ctx - any context of the runtime whose service store is inspected.
94 * @param mount - the retained revision whose subtree owns the service.
95 * @param name - the service name as the preset's rows resolve it.
96 * @returns the implementation, or undefined when the mount provides none.
97 */
98export function serviceForMount<K extends string & keyof Context>(
99 ctx: Context,
100 mount: PresetMount,
101 name: K,
102): Context[K] | undefined {
103 const store = ctx.reflect.store
104 for (const key of Object.getOwnPropertySymbols(store)) {
105 const impl = store[key]
106 /* v8 ignore next -- cordis deletes a store slot on disposal rather than clearing it */
107 if (impl === undefined) continue
108 if (impl.name !== name) continue
109 if (withinFiber(impl.fiber, mount.fiber)) return impl.value as Context[K]
110 }
111 return undefined
112}
113
114/** Rows that did not reach a usable state, each rendered as one diagnostic line. */
115export interface RowAudit {
116 /** Rows that never started or whose import or activation rejected. */
117 readonly failed: string[]
118 /**
119 * Rows waiting for a service the composition does not supply. A Host
120 * provider still activating completes such a row later; only a settled Host
121 * tree tells that case from a genuinely missing service.
122 */
123 readonly pending: string[]
124}
125
126/**
127 * Audit the rows of a mounted subtree.
128 *
129 * Wait for the subtree, then report import failures, activation failures, and
130 * rows waiting for services the composition does not supply.
131 * @param tree - the mounted subtree.
132 * @returns failed and pending rows, both empty when every enabled row is usable.
133 */
134export async function auditRows(tree: EntryTree): Promise<RowAudit> {
135 await tree.await()
136 const failed: string[] = []
137 const pending: string[] = []
138 for (const entry of tree.entries()) {
139 if (entry.disabled) continue
140 const fiber = entry.fiber
141 if (fiber === undefined) {
142 failed.push(`${entry.options.id} (${entry.options.name}): never started`)
143 continue
144 }
145 try {
146 await fiber.await()
147 } catch (error) {
148 const detail = mountDetail(error)
149 failed.push(`${entry.options.id} (${entry.options.name}): ${detail}`)
150 continue
151 }
152 const missing = Object.keys(fiber.inject).filter(name => fiber.ctx.get(name) === undefined)
153 if (missing.length > 0) {
154 pending.push(`${entry.options.id} (${entry.options.name}): waiting for ${missing.join(', ')}`)
155 }
156 }
157 return { failed, pending }
158}
159
160/**
161 * The causes of `error` whose detail its own message does not already carry.
162 *
163 * Aggregate errors carry separate member messages. A wrapper can preserve the
164 * aggregate as its cause without including those messages in its own text.
165 * @param error - the failure to read branches from.
166 * @returns the branches to render beneath `error.message`, possibly empty.
167 */
168function detailBranches(error: Error): readonly unknown[] {
169 if (error instanceof AggregateError) return error.errors
170 return error.cause instanceof AggregateError ? error.cause.errors : []
171}
172
173/**
174 * The reportable text of a mount failure.
175 *
176 * A plugin may reject with an aggregate or wrap one as its cause. Include its
177 * member messages beneath the row diagnostic so each failure is visible.
178 * @param error - the value the mount rejected with.
179 * @returns a single-line-per-cause description.
180 */
181function mountDetail(error: unknown): string {
182 if (!(error instanceof Error)) return String(error)
183 const branches = detailBranches(error)
184 if (branches.length === 0) return error.message
185 return [
186 error.message,
187 ...branches.map(branch => `- ${mountDetail(branch).replaceAll('\n', '\n ')}`),
188 ].join('\n')
189}
190
191/** Load and audit one revision under its registry-owned scope.
192 *
193 * Failed rows and root-realm service leaks reject the mount. Rows waiting for
194 * a Host service stay mounted: they activate by themselves once the provider
195 * finishes, and the registry re-audits them after the Host tree settles.
196 * Inside a profile, compatibility policy decides admission first: a row whose
197 * plugin the profile denies mounts disabled, so the audit reads it as
198 * intentionally inactive instead of reporting a failed import.
199 * @param ctx Scope context inheriting the declaring Loader's resolution base.
200 * @param id Preset identity.
201 * @param plugins Declared Cordis entry list.
202 * @returns The live tree; scope disposal owns its teardown.
203 */
204export async function mountPreset(ctx: Context, id: string, plugins: PresetDefinition['plugins']): Promise<PresetMount> {
205 if (scopeOf(ctx) === undefined) throw new Error('agent-preset: mounting requires a scope')
206 await ctx.fiber.await()
207 const tree = new PresetTree(ctx)
208 ctx.effect(() => () =>{ tree.root.stop() }, 'agent-preset.tree')
209 await tree.root.update(prepareProfileEntries(ctx, plugins as EntryOptions[], ctx.baseUrl))
210 const audit = await auditRows(tree)
211 const leaked = leakedServices(ctx, ctx.fiber)
212 if (audit.failed.length > 0) throw new Error(audit.failed.join('\n'))
213 if (leaked.length > 0) throw new Error(`Preset services require isolate realms: ${leaked.join(', ')}`)
214 const mount = { presetId: id, fiber: ctx.fiber, tree, key: scopeOf(ctx) }
215 return mount
216}