/** * 「主动使用 Agent Teams」策略覆盖行。 * * ## 它解决什么 * * `@deepseek-ai/dsh-experimental-tool-agent-team` 会在**每个 Agent 自己的作用域** * 注册一段固定策略段落 `team:policy`,正文第一句是: * * > Agent Teams is available in this session, but create teammates only when the * > user explicitly asks to use Agent Teams or teammates. * * (`packages/experimental/tool-agent-team/src/index.ts:31`;注册点在同文件 * `:168-172`,经 `agent/created` 钩子写入 `agent.ctx`,见 `:413`。) * * ⇒ 只要这段原样生效,编排者就**必须**等用户每次明说「开启智能体团队」才肯建 * teammate。本预设要的是「默认主动用团队」,所以必须把这段换掉。 * * ## 为什么不能用「同名 section 覆盖」(走过的弯路,别再走) * * 直觉做法是在预设里注册一个**同名** `team:policy` 段落。**在 DSH 里行不通**: * * - 预设 scope 是 agent scope 的 **父级**(祖先),不是子级。 * `AgentPresetRegistry.activate()` 用 `createScope(this.owner, key)` 把预设建在 * 注册表 owner 下(`packages/preset/agent-preset-registry/src/index.ts:104-105`), * 再由 `join()` 执行 `bindScopeParent(agentKey, presetKey)` —— **agent 挂到预设下** * (同文件 `:266`)。所以 `scopeChainOf(agentKey) === [AGENT, PRESET]`。 * - 段落合并遵循「scope 链由远及近、**越近越赢**」 * (`packages/core/scope/src/store.ts:192-217` 的 `chainLayers` + `merge`)。 * 上游注册在 **AGENT**(最近),预设注册在 **PRESET**(祖先)⇒ **祖先必输**。 * * 实测确认:两种拓扑下 agent scope 渲染的都是上游原文;而「查预设 scope 看到覆盖文」 * 是**假阳性**——没有任何 agent 会渲染那个 scope。 * * ## 本行的做法:`system-prompt/assemble` 瀑布 * * `SystemPrompt.assemble()` 在段落组装与作用域链合并**之后**派发 * `system-prompt/assemble` 瀑布,并以**瀑布的返回值**为准 * (`packages/core/system-prompt/src/index.ts:673-681`)。 * * 派发用的是 `scopeTarget(this, scope)`(同文件 `:673-676`),其过滤规则是 * 「监听器的 scope tag 出现在**派发 scope 的祖先链**上即放行」 * (`packages/core/scope/src/index.ts:170-185`)。 * * ⇒ 监听器挂在**预设 scope**(agent scope 的祖先)上,就能收到该预设下每个 agent 的 * 组装结果;而**兄弟预设**的 agent 祖先链里没有这个 tag ⇒ 收不到。 * **既覆盖又隔离**,不需要碰上游那行,也不需要 profile 级特权。 * * ## 已知脆弱点(有测试守着) * * `assemble()` 结尾对 `complete === true` 的段落有**后置恢复**:只要存在完整段落, * 返回值里的 `sections` 会被整体替换成 `[completeSection]`(`:677-683`),本覆盖会被 * **静默丢弃**。当前 cluster 预设没有任何 `complete: true` 段落, * `scripts/test-team-policy.mjs` 会断言这一点。 * * 导出形状与同目录的 `lifecycle-reminder.js` 一致:**具名** `name` + `apply`, * 不带 default。Cordis 的 Loader 从 `plugin.name` 取插件名,裸函数名为 `apply` 时 * 会被置成 undefined。 * * @module cluster-team-policy */ /** Cordis 插件名。 */ export const name = 'cluster-team-policy'; /** 本行依赖的宿主服务。 */ export const inject = ['systemPrompt']; /** * 段落名。必须与上游逐字相同——我们改写的是**同名那一段**的文本,而不是新增一段 * (新增会让提示词里同时出现两份互相矛盾的策略)。 */ const TEAM_POLICY_SECTION = 'team:policy'; /** * 集群模式专用的团队策略正文。 * * 与上游 POLICY 的语义差异只有一处:**把「仅在用户明确要求时创建」改成「默认主动 * 使用」**。上游其余条款(共享工作目录与文件系统、写作用域不重叠、任务板用法、 * `send_message` / `wait_agent` 语义、Lead 必须先收齐再交付)**逐字保留**, * 因为它们是运行时真实约束,删掉会让模型误用工具。 * * 为什么不写「必须委派」:集群模式的价值是**该用团队时就用**,而不是把单文件小改也 * 拆成一支团队。所以措辞是「默认偏向主动建团队」+「琐碎任务自己做完」, * 避免空转团队拖慢又费 token。 */ export const TEAM_POLICY = `Agent Teams 已在本会话启用。**你默认主动使用团队,不需要用户先开口。** 只要任务满足以下任一条,就**直接**用 \`spawn_teammate\` 建 teammate,不要先问、也不要先征得同意: - 能拆成两个以上彼此独立、可并行的工作流 - 需要跨多个文件/模块/数据源做互不重叠的探索或改造 - 需要不同视角交叉验证(例如一人实现、一人从对抗角度复核) - 单项工作量大到并行能明显缩短墙钟时间 判定顺序:先看任务能否拆分 → 能拆就**先建共享任务板**(\`team_task_create\`,写清 subject、验收标准、\`write_scopes\`)→ 再按任务建 teammate 并把任务 id 交给它(\`team_task_update\` 的 claim/assign 与 \`send_message\`)。写作用域必须**互不重叠**;有先后依赖就用 \`blocked_by\` 表达,不要用「等它做完我再看」的口头约定。 只有这几种情况才自己做完、不起团队:单文件小改、单点查询、纯问答、或用户明确要求「你自己做/别开团队」。**不要为了显得忙碌而建团队**——空转的团队比自己做更慢更贵。 创建 teammate 时:\`name\` 用 lower-kebab-case 且能反映职责;\`prompt\` 必须自足(除非你用 \`context: "fork"\`,它看不到你的历史),按「任务 / 期望产出与成功判据 / 允许的工具 / 必须做 / 绝不要做 / 上下文」六段写全,少于 5 行的委派就是太含糊。 The Team Lead and all teammates share the same working directory and filesystem. Edits are immediately visible to every member. Split write work into disjoint scopes, record expected write scopes on shared tasks, and use task dependencies when work must be ordered. Write-scope overlap is advisory, not a lock. Prefer read/edit/write for file changes. If a file operation returns FS_STALE_VERSION, read the current file, rebase your intended change onto the new content, and retry. Bash, formatters, code generators, and scripts are not fully protected by the filesystem version guard; coordinate them explicitly and have the Lead review the final diff and run tests. Use the target returned by spawn_teammate or list_agents for send_message and interrupt_agent, or as owner when assigning or filtering shared tasks. send_message steers a running target at its nearest step boundary and starts or resumes an inactive target. inactive means no turn is executing; it does not describe task completion, success, failure, or waiting for other agents. provisioning means member creation is in progress; failed means member creation failed. A peer message starts with its sender name. A successful send does not mean the recipient has processed it. Shared-task workflow is list, get, claim with the current revision, perform the work, then complete. Task readiness never starts an owner. Before wait_agent, use list_agents and make sure another required member is running or provisioning; use send_message first when the required member is inactive. wait_agent observes only changes after that call starts, never wakes a member, and returns noProgress immediately when no other member can produce a change. Re-list after wakeup or timeout. The Lead must wait for required teammates before giving the final answer.`; /** * 把组装结果里的 `team:policy` 段落替换为集群模式版本。 * * 纯函数,不改动传入对象:返回**新的** assembly 与新的 sections 数组。 * * @param {object} assembly - 上游组装结果。 * @returns {object} 替换后的新组装结果;未命中时原样返回。 */ export function overrideTeamPolicy(assembly) { const sections = Array.isArray(assembly?.sections) ? assembly.sections : undefined; if (sections === undefined) return assembly; let hit = false; const next = sections.map((section) => { if (section?.name !== TEAM_POLICY_SECTION) return section; hit = true; return { ...section, text: TEAM_POLICY }; }); return hit ? { ...assembly, sections: next } : assembly; } /** * 在本(预设)作用域挂上覆盖瀑布。 * * 监听器由 `ctx.effect(...)` 包裹:预设卸载 / 热重载时干净撤销。 * * @param {import('@deepseek-ai/cordis').Context} ctx - cluster 预设作用域上的上下文。 */ export function apply(ctx) { ctx.effect(() => ctx.on('system-prompt/assemble', async (assembly, _context, next) => { const resolved = await next(); return overrideTeamPolicy(resolved); }), 'cluster-team-policy.assemble()'); }