Merge remote-tracking branch 'origin/master' into fix/windows-native-acl-coverage

This commit is contained in:
Huanqi Cao
2026-08-10 21:05:38 +08:00
49 changed files with 909 additions and 92 deletions

View File

@@ -28,7 +28,7 @@ import { bindScopeParent, createScope, scopeOf, type Scope, type ScopeKey, type
import { settingsNamespace, type SettingsScope, type default as SettingsService } from '@deepseek-ai/dsh-settings'
import { discoverPresets } from './discovery.ts'
import { copyComposition, deleteComposition, readComposition } from './authoring.ts'
import { mountPreset, serviceForAgent } from './mount.ts'
import { mountPreset, serviceForAgent, standingMountFor } from './mount.ts'
import { PresetExistsError } from './authoring.ts'
import { PresetMountError, UnknownPresetError, type AgentPreset, type Config } from './types.ts'
@@ -51,8 +51,8 @@ export {
METADATA_FILE, readPresetMetadata, renderPresetMetadata, type PresetMetadata,
} from './metadata.ts'
export {
inactiveRows, leakedServices, livePresetMounts, mountPreset, serviceForAgent,
type PresetMount,
inactiveRows, leakedServices, livePresetMounts, mountPreset, serviceForAgent, standingMountFor,
type JoinedPresetMount, type PresetMount,
} from './mount.ts'
export {
copyComposition, deleteComposition, InvalidPresetIdError, PresetExistsError,
@@ -238,6 +238,56 @@ export class AgentPresets extends Service {
return preset
}
/**
* Join one agent to the SAME standing composition another already runs on.
*
* This is how a child agent inherits its parent's capabilities. It is a bind,
* not a mount: the parent's generation is already composed, so the child gets
* that exact instance — the same plugin objects, the same tool registrations,
* the same prompt sections. Re-resolving the parent's preset by id instead
* would re-read the roster, and a composition file edited since the parent
* started would hand the child a DIFFERENT generation than the one its
* parent's history was produced under (and a preset deleted since would fail
* the child outright while its parent keeps running).
*
* Synchronous, and with no composition failure mode of its own — it reads no
* roster, mounts nothing, and touches no file — which is what lets a child
* creation window use it: the two in-process subagent drivers compose their
* children inside a synchronous `setup`. It still rejects a caller error, as
* the `@throws` below record.
*
* A parent that joined no preset — a rosterless deployment — yields no join
* and no error: there, the model-facing rows sit in the host composition and
* the child already sees them through the global layer.
* @param agentCtx - the joining agent's scope context.
* @param parentCtx - the scope context of the agent whose composition to join.
* @returns the preset id joined, or undefined when the parent joined none.
* @throws when `agentCtx` carries no scope, or has already joined a preset.
*/
composeFrom(agentCtx: Context, parentCtx: Context): string | undefined {
const agentKey = scopeOf(agentCtx)
if (agentKey === undefined) {
throw new Error('agent-presets: refusing to compose an unscoped context; the scope key is what joins an agent to its preset')
}
const standing = standingMountFor(parentCtx)
if (standing === undefined) return undefined
this.bindings.set(agentKey, bindScopeParent(agentKey, standing.key))
return standing.presetId
}
/**
* The preset one live agent runs on.
*
* Read from the live scope chain rather than from the session, so it answers
* for an agent whose session has not recorded a preset yet — a child agent
* whose durable header is being built from its parent's composition.
* @param agentCtx - the agent's scope context.
* @returns the preset id, or undefined when the agent joined none.
*/
composedPreset(agentCtx: Context): string | undefined {
return standingMountFor(agentCtx)?.presetId
}
/** Whether this deployment configures a root locally authored presets go to. */
get authorable(): boolean {
return this.config.roots.some(root => root.trust === 'user')

View File

@@ -202,6 +202,33 @@ export function leakedServices(ctx: Context, mount: Fiber): string[] {
return leaked.sort((left, right) => left.localeCompare(right))
}
/** A live standing mount located through one agent already joined to it. */
export type JoinedPresetMount = PresetMount & {
/** The standing key, definite because it is what the lookup matched on. */
readonly key: ScopeKey
}
/**
* The standing composition one agent is joined to.
*
* The agent's own key is parented to its preset's standing key, so the mount
* is found by matching that parent rather than by walking up from the agent —
* the mount is not under the agent's fiber. An agent that joined no preset —
* a deployment composing no roster, or a child agent before its join — has no
* parent link and resolves to undefined.
* @param agentCtx - the agent's scope context.
* @returns the mount the agent joined, or undefined when it joined none.
*/
export function standingMountFor(agentCtx: Context): JoinedPresetMount | undefined {
const agentKey = scopeOf(agentCtx)
if (agentKey === undefined) return undefined
const standingKey = scopeParentOf(agentKey)
if (standingKey === undefined) return undefined
return livePresetMounts().find(
(candidate): candidate is JoinedPresetMount => candidate.key === standingKey,
)
}
/**
* One agent's instance of a service its preset mounted.
*
@@ -231,14 +258,7 @@ export function serviceForAgent<K extends string & keyof Context>(
agent: { ctx: Context },
name: K,
): Context[K] | undefined {
// The agent's own key is parented to its preset's standing key; the mount
// is no longer under the agent's fiber, so the search roots at the standing
// mount instead of walking up from the agent.
const agentKey = scopeOf(agent.ctx)
if (agentKey === undefined) return undefined
const standingKey = scopeParentOf(agentKey)
if (standingKey === undefined) return undefined
const mount = livePresetMounts().find(candidate => candidate.key === standingKey)
const mount = standingMountFor(agent.ctx)
if (mount === undefined) return undefined
const store = ctx.reflect.store
for (const key of Object.getOwnPropertySymbols(store)) {