作用域(scope)是那些注册表和循环在其上构建每智能体作用域的无依赖库原语(packages/core/scope/src/index.ts 与 store.ts)。它是 core 主干链上唯一不是服务(Service)的包,在模块图里位于 session/ 与 system-prompt/ 之下,正是为了让这两个包能消费它而避免环。它赋予一个注册上下文两个同时成立的含义:按智能体的可见性与共享的生命周期所有权。
| 包 | 负责 |
|---|---|
@deepseek-ai/dsh-scope | ScopeKey、Scope、scopeTarget、createScope、bindScopeParent、ScopedLayers 存储 |
@deepseek-ai/dsh-agent | 运行时注册表与 agent.ctx(基于 scope 构建) |
全局与作用域:两个扁平层级
可见性没有多层继承树。注册项要么全局(对每个智能体可见),要么作用域化(恰好归属一个作用域键)。术语表说得很明白:"两个层级,扁平:作用域注册不向下继承到子智能体;子树行为用血统(lineage)数据表达,而不用作用域结构。"
ScopedLayers 存储执行这条规则:它拥有一份懒求值构造的全局层,以及按 ScopeKey 懒创建的精确作用域层。
export class ScopedLayers<L extends ScopeLayer> {
/** The eagerly constructed context-global layer. */
readonly global: L
private readonly scoped = new Map<ScopeKey, L>()
}ScopeKey 是不透明、按身份比较的对象。随产品发布的循环把活跃的 Agent 对象当作自己的键,但原语从不检查对象内容——任意按身份比较的 object 都行。
精确作用域层,以及带父链的作用域注册表
在扁平的"两层"模型内部,仍然存在一条父链(scopeParents),用于两个作用域注册表目的:作用域分层(chainLayers/merge)与事件准入(scopeTarget)。作用域键可以通过 bindScopeParent(parent) 绑定一个包围作用域,该绑定做了环检测,且只能由原始绑定者重新链接。保持"扁平"的规则是:事件与注册沿链向上流动,绝不向下——属于包围作用域的监听器会收到每个后代的事件,但位于分发键下方的标签保持排除。
分发载体:scopeTarget
事件分发按载体路由。scopeTarget(base, key) 构建一个不透明接收器,它保留 base 已有的 Cordis 过滤器,全局接收未打标签的监听器,并且接收匹配键或任一祖先的已打标签监听器。
export function scopeTarget<T extends object>(base: T, key: ScopeKey | undefined): Scoped<T> {
const baseFilter = (base as { [CordisContext.filter]?: (ctx: Context) => boolean })[CordisContext.filter]
const carrier = {
[CordisContext.filter](ctx: Context): boolean {
if (baseFilter !== undefined && !baseFilter.call(base, ctx)) return false
const tag = scopeOf(ctx)
if (tag === undefined) return true // 未打标签监听器全局接收
for (let cursor = key; cursor !== undefined; cursor = scopeParents.get(cursor)) {
if (cursor === tag) return true // 主题自身键,或任一祖先
}
return false
},
}
carrierKeys.set(carrier, key)
return carrier as unknown as Scoped<T>
}注册表主题事件不做过滤。 agent/created、agent/disposed、agent/status 等事件都把自己的 this 声明为以 scopeTarget(agent, agent) 构建的 Scoped<Agent> 载体——主题就是键,因此分发包含一个作用域过滤,能到达该主题的监听器。但诸如 system-prompt/change 之类的通知以不带作用域的主题发出,因此能到达所有人(全局变化会影响每个作用域)。
拥有式注册上下文:Scope 类表面
createScope(ctx, key) 铸出一个作用域化的 Context,继承挂载插件的依赖 API,并拥有所有经由它进行的注册。真实的 Scope 类表面:
export interface Scope {
/** Context through which scope-owned registrations are made. */
ctx: Context
/** Exact Cordis disposer, used when nesting this scope in an ordered composite effect. */
rawDispose: () => Promise<void> | void
/** Dispose every scope-owned registration; racing calls await the same completion. */
dispose(): Promise<void>
}export function createScope(ctx: Context, key: ScopeKey, options?: CreateScopeOptions): Scope {
if (options?.parent !== undefined) bindScopeParent(key, options.parent)
const fiber = ctx.plugin(scope)
const scoped: Context = fiber.ctx.extend({ [kScope]: key })
let disposing: Promise<void> | undefined
return {
ctx: scoped,
rawDispose: fiber.dispose,
dispose: () => (disposing ??= quiesceFiber(fiber)),
}
}rawDispose 保留有序组合效应所需的精确 Cordis 释放器身份(异步智能体工厂用它来把智能体的销毁与其会话按顺序联动);dispose() 是直接调用者与竞态调用者共享的静止边界。
作用域分发规则
作用域分发规则用一句话概括就是:主题载体接收未打标签的监听器,加上该主题自身(及其祖先)的已打标签监听器。 具体来说,ctx.agents.register(agent) 以 this = scopeTarget(agent, agent) 发出 agent/created,因此智能体作用域的监听器(通过 agent.ctx 注册)只会收到该智能体的生命周期事件,而未打标签的监听器能看到全部。
遮蔽:最特异者胜出
遮蔽是 persona 与每智能体工具变体的工作机制。在同一个注册表内,merge(scope, pick) 先整理全局具名条目,再叠加作用域链上的遮蔽,祖先最远者在前,因此最近的(最特异的)作用域条目在一个同名键上胜出:
merge<V>(scope: ScopeKey | undefined, pick: (layer: L) => NamedEntries<V>): Map<string, V> {
const merged = new Map(pick(this.global).entries())
for (const layer of this.chainLayers(scope)) {
for (const [name, value] of pick(layer).entries()) merged.set(name, value)
}
return merged
}作用域化的 deployment:persona 节会为该作用域单独替换全局的 persona;同名的作用域工具会为自己的智能体隐藏全局孪生。.peek(scope) 刻意忽略链——它只读取一个作用域自己的贡献,绝不读取祖先的,因此限制与守卫被严格限定在该作用域。
一个可运行的例子:persona 与每智能体工具
全局与作用域与否在提示组装里变得具体。部署通过普通插件配置挂载一个全局 persona(order-0 的 deployment:persona 节)。某个智能体预设想要自己的 persona 与一个额外工具:
// 宿主平面(全局,每个智能体可见)
systemPrompt.section({ name: PERSONA_SECTION, order: PERSONA_ORDER, text: globalPersona })
systemPrompt.tools(() => ({ schemas: [sharedTool] }))
// 智能体平面(agent ctx,只对该智能体可见)
agentCtx.systemPrompt.section({ name: PERSONA_SECTION, order: 0, text: presetPersona }) // 遮蔽全局
agentCtx.systemPrompt.tools(() => ({ schemas: [agentTool] })) // 作用域合并
agentCtx.tools.restrict({ deny: [sharedTool.name] }) // 过滤全局merge(agent, pick) 产出按名字胜出的 persona 节(作用域者替换全局者),并且两个工具提供者都参与(按组装流程,全局提供者与匹配的作用域提供者都运行)。restrict 之后,sharedTool 对该智能体既不可见也不可执行。凡经 agent.ctx 注册的内容都会在智能体的作用域销毁时一并解除——这同一个机制也拥有工具、提示与限制的注册。
限制:按交集组合
限制(tools.restrict(filter))为一个作用域过滤全局工具集,多个限制按交集组合(一个 allow/deny 过滤器必须让该工具通过每一个匹配的限制才保持可见)。被过滤掉的全局工具既不出现在提示中,也拒绝执行——与一个从未存在的工具毫无区别;作用域本地的工具注册在该过滤器之后合并。在注册表的 ScopedLayers 里,限制住在该作用域自己的层(.peek(scope)),忽略链,因此一个智能体的限制绝不会静默拾取祖先的。
设置窗口
设置窗口(setup window) 是创建者组合一个智能体作用域世界的创建槽位:CreateAgentOptions.setup(agentCtx)。它在作用域和智能体对象都已存在、但智能体或会话尚未发布、agent/session-start 尚未触发、第一个提示也尚未组装的时候运行。Setup 只注册;它从不驱动智能体。setup 被拒绝、提交抛出、或拥有者被销毁,都会回滚事务且不发布任一个 id。
血统是数据,不是作用域
血统——父子事实——以数据承载,绝非作用域结构:parentSession、持久的 delegationDepth、运行时的 subagentDepth。因为作用域注册不向下继承到子智能体,部署通过读取这些数据来表达子树行为,而不是依赖作用域父子关系。作用域键上的父链只用于作用域注册表分层与跨一个组合好的常驻作用域的事件准入,这与子智能体血统是不同的关注点。
延伸阅读
- 智能体循环 —— 循环如何以活跃的
ReactLoopAgent为键铸出自身作用域。 - 会话管理 —— 拥有每个智能体事件日志的作用域存储。
- 系统提示组装 —— 作用域节/变量如何遮蔽全局项。
- 工具展示 —— 作用域工具、限制过滤与
tools/presentAs。 - 仓库源码:
packages/core/scope/src/index.ts、packages/core/scope/src/store.ts。 - 官方脚手架:
docs/subsystems/scope.md、docs/glossary.md(agent-scope、lineage、shadowing、restriction、setup-window 条目)。