mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
refactor: rename agent teams service key
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-agent-teams.md
|
||||
2026-08-05-agent-teams.md: 7a08dec2fc841e30fd8564f533086559a786d180
|
||||
2026-08-05-agent-teams.zh.md: dcab1bbe764da1ce51dc4dad56a180f22b9fffa3
|
||||
2026-08-05-agent-teams.md: d37777a0cca467edcec5d38999aee53aaf14dc36
|
||||
2026-08-05-agent-teams.zh.md: 78bb212d687f89da0911983ea9d7fa5e5bbe95a7
|
||||
|
||||
@@ -16,7 +16,7 @@ The model-visible Team tools remain opt-in so the default tool catalog and simpl
|
||||
|
||||
Every ordinary runtime root is the implicit Lead of a Team identified by that root's `SessionId`. The Team has no creation event: its Lead pseudo-row exists by identity, while durable state begins with the first member, message, or task event. A roster is flat and contains at most the configured number of immutable lowercase-kebab-case names. Each teammate is a continuable direct child with a reserved Session id; only the Lead creates or interrupts teammates. Ordinary provider-owned subagents outside the roster are not Team members, and an ordinary fork is a new root whose inherited Team records are excluded by their ancestor `TeamId`.
|
||||
|
||||
The implementation is split into `@deepseek-ai/dsh-experimental-agent-team`, which owns `ctx.teams` and durable semantics, and `@deepseek-ai/dsh-experimental-tool-agent-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. Deployments mount both plugins explicitly and may disable legacy continuable controls with the same model-visible names. The explicit delegation policy permits Team creation only when the user asks for Agent Teams or teammates. Both packages are private members of `packages/experimental/`; the [experimental package decision](../architecture/2026-08-18-experimental-agent-teams-packages.md) owns release exclusion, dependency isolation, and promotion.
|
||||
The implementation is split into `@deepseek-ai/dsh-experimental-agent-team`, which owns `ctx.agentTeams` and durable semantics, and `@deepseek-ai/dsh-experimental-tool-agent-team`, which owns scoped schemas and model guidance. Every Team tool declares its complete result schema and renders that value as compact JSON, so the compiler checks each `execute` against what the model is promised and no result spends tokens on indentation. Deployments mount both plugins explicitly and may disable legacy continuable controls with the same model-visible names. The explicit delegation policy permits Team creation only when the user asks for Agent Teams or teammates. Both packages are private members of `packages/experimental/`; the [experimental package decision](../architecture/2026-08-18-experimental-agent-teams-packages.md) owns release exclusion, dependency isolation, and promotion.
|
||||
|
||||
The Lead must wait for required work before its final answer. Process teardown remains the final lifecycle owner and drains continuation Activations; a Team task owner is durable state and is not automatically released by idle, interruption, or process exit.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ subagent seam 已提供 fresh/fork provider、持久 child Session、FIFO foll
|
||||
|
||||
每个普通运行时 Root 都是一个隐式 Team 的 Lead,Team id 等于该 Root 的 `SessionId`。Team 没有 creation event:Lead pseudo-row 由身份直接存在,持久状态从第一条 member、message 或 task event 开始。roster 是扁平结构,最多包含配置数量、不可变且采用小写 kebab-case 的名字。每个 teammate 都是使用预留 Session id 的 continuable 直接 child;只有 Lead 可以创建或 interrupt teammate。roster 外由 provider 管理的普通 subagent 不是 Team member;普通 fork 是新的 Root,继承的 Team 记录会因 ancestor `TeamId` 被排除。
|
||||
|
||||
实现拆分为 `@deepseek-ai/dsh-experimental-agent-team` 与 `@deepseek-ai/dsh-experimental-tool-agent-team`:前者负责 `ctx.teams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件,并可禁用具有相同模型可见名称的旧 continuable control。显式 delegation 策略只允许在用户要求 Agent Teams 或 teammate 时创建 Team。 两个包都是 `packages/experimental/` 的私有成员;[实验性包决策](../architecture/2026-08-18-experimental-agent-teams-packages.md)负责发布排除、依赖隔离与 promotion。
|
||||
实现拆分为 `@deepseek-ai/dsh-experimental-agent-team` 与 `@deepseek-ai/dsh-experimental-tool-agent-team`:前者负责 `ctx.agentTeams` 和持久语义,后者负责 scoped schema 与模型指引。每个 Team 工具都声明完整的结果 schema,并把该值渲染为紧凑 JSON,因此编译器会检查每个 `execute` 是否符合对模型的承诺,也没有结果把 token 花在缩进上。部署显式挂载两个插件,并可禁用具有相同模型可见名称的旧 continuable control。显式 delegation 策略只允许在用户要求 Agent Teams 或 teammate 时创建 Team。 两个包都是 `packages/experimental/` 的私有成员;[实验性包决策](../architecture/2026-08-18-experimental-agent-teams-packages.md)负责发布排除、依赖隔离与 promotion。
|
||||
|
||||
Lead 必须等待所需工作后才能给出最终答案。进程 teardown 仍是最终生命周期 owner,并会 drain continuation Activation;Team task owner 是持久状态,不会因 idle、interrupt 或进程退出自动释放。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/architecture.md
|
||||
architecture.md: 2a51992a4f99d1827259c0a2ca2daa309f0ddd95
|
||||
architecture.zh.md: d1629a088ed8d2fe61a7e20ab2ab1fd3709abd50
|
||||
architecture.md: 3c5e2bd74c1b1519128dac6f5d8502ba8c70c0e2
|
||||
architecture.zh.md: 39decd41b677db7c8f160c9e9888ec4ea30401c2
|
||||
|
||||
@@ -101,7 +101,7 @@ A **seam** is a swappable capability with three roles: a **Service Definition**
|
||||
|
||||
Seams are why one provider swap changes the whole product. Filesystem and subprocess providers share one execution world, so pointing them at a remote sandbox moves Bash, PTY, and LSP with them, with no provider forks. [Subagent providers](subsystems/subagent.md) vary just as widely behind one interface, from a fresh child agent to a delegated turn in another product.
|
||||
|
||||
[Experimental Agent Teams](subsystems/team.md) is a private opt-in coordination seam on `ctx.teams`, with a durable roster, task board, and mailbox layered over continuable subagents.
|
||||
[Experimental Agent Teams](subsystems/team.md) is a private opt-in coordination seam on `ctx.agentTeams`, with a durable roster, task board, and mailbox layered over continuable subagents.
|
||||
|
||||
## Where new behavior goes
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ turn/end
|
||||
|
||||
seam 正是替换一个提供方就能改变整个产品的原因。文件系统与进程提供方共享同一个执行世界,因此把它们指向远程沙箱,也就把 Bash、PTY 和 LSP 一并搬了过去,无需提供方专用 fork。[subagent 提供方](subsystems/subagent.md)在同一个接口之后同样千差万别,从新建一个子 agent,到把一个轮次委派给另一个产品。
|
||||
|
||||
[实验性 Agent Teams](subsystems/team.md) 是 `ctx.teams` 上的私有显式启用协作 seam,在可继续 subagent 之上提供持久 roster、任务板和 mailbox。
|
||||
[实验性 Agent Teams](subsystems/team.md) 是 `ctx.agentTeams` 上的私有显式启用协作 seam,在可继续 subagent 之上提供持久 roster、任务板和 mailbox。
|
||||
|
||||
## 新行为的归属位置
|
||||
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
backgroundMode: one-shot
|
||||
maxDepth: 1
|
||||
- insert:
|
||||
- id: team
|
||||
- id: agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-agent-team'
|
||||
- id: tool-team
|
||||
- id: tool-agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-tool-agent-team'
|
||||
- id: team-fixture-llm
|
||||
name: './tests/fixtures/team-llm.mjs'
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/experimental/README.md
|
||||
README.md: 5773cccdf189324f0f29b5aa2840960fa956c60b
|
||||
README.zh.md: 12f7cbd5160507cf46d83c115591ed901ce82b7a
|
||||
README.md: 0e92ebd2bd959ac807830400dd57807b87b1cbe2
|
||||
README.zh.md: a1751c39f53bf8f6c0a9c623aba57355f35c2681
|
||||
|
||||
@@ -6,7 +6,7 @@ This group contains prototypes and internal-only Cordis plugins that use the rep
|
||||
|
||||
| Package | Role | ctx key |
|
||||
|---|---|---|
|
||||
| `agent-team/` | Implicit-root Agent Teams roster, durable peer mailbox, shared task DAG, and runtime coordination | `ctx.teams` |
|
||||
| `agent-team/` | Implicit-root Agent Teams roster, durable peer mailbox, shared task DAG, and runtime coordination | `ctx.agentTeams` |
|
||||
| `tool-agent-team/` | Scoped model-facing Agent Teams tools and collaboration guidance | — |
|
||||
|
||||
The [subtree rules](AGENTS.md) define dependency isolation, release exclusion, and promotion.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
| 包 | 职责 | ctx key |
|
||||
|---|---|---|
|
||||
| `agent-team/` | 隐式 root Agent Teams roster、持久 peer mailbox、共享任务 DAG 与运行时协调 | `ctx.teams` |
|
||||
| `agent-team/` | 隐式 root Agent Teams roster、持久 peer mailbox、共享任务 DAG 与运行时协调 | `ctx.agentTeams` |
|
||||
| `tool-agent-team/` | 按 Agent 作用域提供的 Agent Teams 模型工具与协作指引 | — |
|
||||
|
||||
[子树规则](AGENTS.md)规定依赖隔离、发布排除与 promotion。
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/experimental/agent-team/README.md
|
||||
README.md: 6cf468648b7e9df2d40d358250ef65e2f1b742fb
|
||||
README.zh.md: bd43619cb0465edcf591e49a0984d8cb6176cce6
|
||||
README.md: 96983d58d5670866d113d4d0d1af57fe500bc5fc
|
||||
README.zh.md: 940cf1c9573e12ee0b562070fa67a4c829eb2e6d
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Implicit-root Agent Teams domain. `ctx.teams` owns a flat Lead/teammate roster, a durable peer mailbox, and a shared task DAG in the Lead Session log. The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns the coordination and isolation decisions; the [Team subsystem catalog](../../../docs/subsystems/team.md) records the literal durable shapes and service API.
|
||||
Implicit-root Agent Teams domain. `ctx.agentTeams` owns a flat Lead/teammate roster, a durable peer mailbox, and a shared task DAG in the Lead Session log. The [Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns the coordination and isolation decisions; the [Team subsystem catalog](../../../docs/subsystems/team.md) records the literal durable shapes and service API.
|
||||
|
||||
## Config
|
||||
|
||||
```yaml
|
||||
- id: team
|
||||
- id: agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-agent-team'
|
||||
config:
|
||||
maxMembers: 8
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
隐式 Root Agent Teams 领域。`ctx.teams` 在 Lead Session 日志中维护扁平的 Lead/teammate roster、持久 peer mailbox 与共享任务 DAG。[Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md)负责协作和隔离决策;[Team 子系统目录](../../../docs/subsystems/team.md)记录持久数据的字面形态与服务 API。
|
||||
隐式 Root Agent Teams 领域。`ctx.agentTeams` 在 Lead Session 日志中维护扁平的 Lead/teammate roster、持久 peer mailbox 与共享任务 DAG。[Agent Teams Agent Note](../../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md)负责协作和隔离决策;[Team 子系统目录](../../../docs/subsystems/team.md)记录持久数据的字面形态与服务 API。
|
||||
|
||||
## 配置
|
||||
|
||||
```yaml
|
||||
- id: team
|
||||
- id: agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-agent-team'
|
||||
config:
|
||||
maxMembers: 8
|
||||
|
||||
@@ -34,7 +34,7 @@ export { foldTeam } from './fold.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
teams: TeamService
|
||||
agentTeams: TeamService
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export class TeamService extends Service {
|
||||
private readonly tasks: TeamTaskBoard
|
||||
|
||||
constructor(ctx: Context, config: Config = {}) {
|
||||
super(ctx, 'teams')
|
||||
super(ctx, 'agentTeams')
|
||||
this.config = {
|
||||
maxMembers: positiveLimit('maxMembers', config.maxMembers ?? DEFAULT_MAX_MEMBERS),
|
||||
maxTasks: positiveLimit('maxTasks', config.maxTasks ?? DEFAULT_MAX_TASKS),
|
||||
@@ -110,7 +110,7 @@ export class TeamService extends Service {
|
||||
const membership = this.roster.tryMembership(agent)
|
||||
if (membership !== undefined) this.activity.notify(membership.id)
|
||||
})
|
||||
ctx.effect(() => () => this.disposeRuntime(), 'teams.runtimeLifecycle()')
|
||||
ctx.effect(() => () => this.disposeRuntime(), 'agentTeams.runtimeLifecycle()')
|
||||
for (const agent of ctx.agents.list()) this.scheduleRecovery(agent)
|
||||
}
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ for (const backend of backends) {
|
||||
expect(failedMember?.error).toContain('child Session recovery failed')
|
||||
}, { timeout: 5_000 })
|
||||
|
||||
const receipt = await second.ctx.teams.sendMessage(activeHandle.agent, {
|
||||
const receipt = await second.ctx.agentTeams.sendMessage(activeHandle.agent, {
|
||||
target: 'recoverable',
|
||||
content: [{ type: 'text', text: 'resume after reconciliation' }],
|
||||
delivery: 'wakeup',
|
||||
@@ -274,7 +274,7 @@ for (const backend of backends) {
|
||||
|
||||
const first = await stack(backend, storageRoot, [textResponse('initial teammate answer')])
|
||||
const firstLead = first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' })
|
||||
const started = await first.ctx.teams.spawnTeammate(firstLead, {
|
||||
const started = await first.ctx.agentTeams.spawnTeammate(firstLead, {
|
||||
name: 'mail-worker',
|
||||
description: 'mail recovery worker',
|
||||
prompt: [{ type: 'text', text: 'finish before restart' }],
|
||||
@@ -283,7 +283,7 @@ for (const backend of backends) {
|
||||
signal: SIGNAL,
|
||||
})
|
||||
await vi.waitFor(() => { expect(first.ctx.agents.get(started.member.id)).toBeUndefined() }, { timeout: 5_000 })
|
||||
const quiet = await first.ctx.teams.sendMessage(firstLead, {
|
||||
const quiet = await first.ctx.agentTeams.sendMessage(firstLead, {
|
||||
target: 'mail-worker',
|
||||
content: [{ type: 'text', text: 'durable quiet context' }],
|
||||
delivery: 'quiet',
|
||||
@@ -304,7 +304,7 @@ for (const backend of backends) {
|
||||
})
|
||||
expect(second.ctx.agents.get(started.member.id)).toBeUndefined()
|
||||
|
||||
const waking = await second.ctx.teams.sendMessage(rootHandle.agent, {
|
||||
const waking = await second.ctx.agentTeams.sendMessage(rootHandle.agent, {
|
||||
target: 'mail-worker',
|
||||
content: [{ type: 'text', text: 'resume after restart' }],
|
||||
delivery: 'wakeup',
|
||||
@@ -335,7 +335,7 @@ for (const backend of backends) {
|
||||
|
||||
const first = await stack(backend, storageRoot, [textResponse('initial teammate answer')])
|
||||
const firstLead = first.ctx.agentLoop.create(rootId, { provider: 'mock', model: 'mock' })
|
||||
const started = await first.ctx.teams.spawnTeammate(firstLead, {
|
||||
const started = await first.ctx.agentTeams.spawnTeammate(firstLead, {
|
||||
name: 'dedup-worker',
|
||||
description: 'mail deduplication worker',
|
||||
prompt: [{ type: 'text', text: 'finish before the crash window' }],
|
||||
|
||||
@@ -85,7 +85,7 @@ interface TeamServiceInternals {
|
||||
|
||||
/** White-box access follows the runtime owners so coverage does not widen the service API. */
|
||||
function teamInternals(ctx: Context): TeamServiceInternals {
|
||||
return ctx.teams as unknown as TeamServiceInternals
|
||||
return ctx.agentTeams as unknown as TeamServiceInternals
|
||||
}
|
||||
|
||||
function spawn(
|
||||
@@ -95,7 +95,7 @@ function spawn(
|
||||
options: { context?: 'fresh' | 'fork'; provider?: string } = {},
|
||||
) {
|
||||
const context = options.context ?? 'fresh'
|
||||
return ctx.teams.spawnTeammate(lead, {
|
||||
return ctx.agentTeams.spawnTeammate(lead, {
|
||||
name,
|
||||
description: `${name} responsibility`,
|
||||
prompt: content(`${name} initial`),
|
||||
@@ -187,7 +187,7 @@ describe('Team identity and provisioning', () => {
|
||||
|
||||
expect((await ctx.sessionPersistence.inspect(forked.member.id)).meta.seedLength).toBeGreaterThan(0)
|
||||
expect((await ctx.sessionPersistence.inspect(fresh.member.id)).meta.seedLength ?? 0).toBe(0)
|
||||
expect(ctx.teams.listMembers(lead).map(row => [row.name, row.context, row.status])).toEqual([
|
||||
expect(ctx.agentTeams.listMembers(lead).map(row => [row.name, row.context, row.status])).toEqual([
|
||||
['lead', undefined, 'idle'],
|
||||
['fork-worker', 'fork', 'inactive'],
|
||||
['fresh-worker', 'fresh', 'inactive'],
|
||||
@@ -288,7 +288,7 @@ describe('Team identity and provisioning', () => {
|
||||
const { ctx, lead } = await setup([], { maxMembers: 1 })
|
||||
await expect(spawn(ctx, lead, 'failed-worker', { provider: 'missing' })).rejects.toThrow()
|
||||
|
||||
expect(ctx.teams.listMembers(lead)[1]).toMatchObject({
|
||||
expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({
|
||||
name: 'failed-worker',
|
||||
status: 'failed',
|
||||
provider: 'missing',
|
||||
@@ -301,11 +301,11 @@ describe('Team identity and provisioning', () => {
|
||||
const first = await setup([])
|
||||
vi.spyOn(first.ctx.subagents, 'startContinuable').mockRejectedValueOnce('string provider failure')
|
||||
await expect(spawn(first.ctx, first.lead, 'string-failure')).rejects.toBe('string provider failure')
|
||||
expect(first.ctx.teams.listMembers(first.lead)[1]).toMatchObject({
|
||||
expect(first.ctx.agentTeams.listMembers(first.lead)[1]).toMatchObject({
|
||||
status: 'failed',
|
||||
diagnostics: ['string provider failure'],
|
||||
})
|
||||
await expect(first.ctx.teams.sendMessage(first.lead, {
|
||||
await expect(first.ctx.agentTeams.sendMessage(first.lead, {
|
||||
target: 'string-failure', content: content('cannot deliver'), delivery: 'quiet', signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_MEMBER_NOT_FOUND' })
|
||||
|
||||
@@ -389,16 +389,16 @@ describe('Team identity and provisioning', () => {
|
||||
const started = await spawn(ctx, lead, 'worker')
|
||||
const worker = await waitRunning(ctx, started.member.id)
|
||||
await expect(spawn(ctx, worker, 'nested')).rejects.toMatchObject({ code: 'TEAM_LEAD_REQUIRED' })
|
||||
expect(() => ctx.teams.interrupt(worker, 'worker')).toThrow(expect.objectContaining({ code: 'TEAM_LEAD_REQUIRED' }))
|
||||
expect(ctx.teams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' })
|
||||
expect(() => ctx.agentTeams.interrupt(worker, 'worker')).toThrow(expect.objectContaining({ code: 'TEAM_LEAD_REQUIRED' }))
|
||||
expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' })
|
||||
await waitNoAgent(ctx, worker.id)
|
||||
expect(ctx.teams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'inactive' })
|
||||
expect(() => ctx.teams.interrupt(lead, 'lead')).toThrow(expect.objectContaining({ code: 'TEAM_INVALID_TARGET' }))
|
||||
expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'inactive' })
|
||||
expect(() => ctx.agentTeams.interrupt(lead, 'lead')).toThrow(expect.objectContaining({ code: 'TEAM_INVALID_TARGET' }))
|
||||
})
|
||||
|
||||
it('validates teammate text fields and pre-provisioning cancellation', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
await expect(ctx.teams.spawnTeammate(lead, {
|
||||
await expect(ctx.agentTeams.spawnTeammate(lead, {
|
||||
name: 'empty-description',
|
||||
description: ' ',
|
||||
prompt: content('unused'),
|
||||
@@ -406,7 +406,7 @@ describe('Team identity and provisioning', () => {
|
||||
provider: 'spawn',
|
||||
signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
await expect(ctx.teams.spawnTeammate(lead, {
|
||||
await expect(ctx.agentTeams.spawnTeammate(lead, {
|
||||
name: 'empty-provider',
|
||||
description: 'valid description',
|
||||
prompt: content('unused'),
|
||||
@@ -416,7 +416,7 @@ describe('Team identity and provisioning', () => {
|
||||
})).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
const controller = new AbortController()
|
||||
controller.abort(new TeamError('cancelled before provisioning', 'TEST_CANCELLED'))
|
||||
await expect(ctx.teams.spawnTeammate(lead, {
|
||||
await expect(ctx.agentTeams.spawnTeammate(lead, {
|
||||
name: 'cancelled-worker',
|
||||
description: 'never provisioned',
|
||||
prompt: content('unused'),
|
||||
@@ -429,7 +429,7 @@ describe('Team identity and provisioning', () => {
|
||||
|
||||
it('treats an ordinary fork as a new Root Team and filters inherited Team state', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
await ctx.teams.createTask(lead, { subject: 'parent task', description: 'belongs to parent' })
|
||||
await ctx.agentTeams.createTask(lead, { subject: 'parent task', description: 'belongs to parent' })
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('ordinary-fork'),
|
||||
seed: lead.session.events,
|
||||
@@ -437,7 +437,7 @@ describe('Team identity and provisioning', () => {
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
|
||||
expect(ctx.teams.membership(handle.agent)).toMatchObject({
|
||||
expect(ctx.agentTeams.membership(handle.agent)).toMatchObject({
|
||||
id: TeamId(handle.agent.id),
|
||||
role: 'lead',
|
||||
name: 'lead',
|
||||
@@ -455,20 +455,20 @@ describe('Team identity and provisioning', () => {
|
||||
signal: SIGNAL,
|
||||
})
|
||||
const live = ctx.agents.get(started.childId)
|
||||
if (live !== undefined) expect(ctx.teams.tryMembership(live)).toBeUndefined()
|
||||
if (live !== undefined) expect(ctx.agentTeams.tryMembership(live)).toBeUndefined()
|
||||
await waitNoAgent(ctx, started.childId)
|
||||
expect(() => ctx.teams.membership(lead)).not.toThrow()
|
||||
expect(() => ctx.agentTeams.membership(lead)).not.toThrow()
|
||||
|
||||
const impostor = { ...lead } as Agent
|
||||
expect(ctx.teams.tryMembership(impostor)).toBeUndefined()
|
||||
expect(() => ctx.teams.membership(impostor)).toThrow(expect.objectContaining({ code: 'TEAM_NOT_MEMBER' }))
|
||||
expect(ctx.agentTeams.tryMembership(impostor)).toBeUndefined()
|
||||
expect(() => ctx.agentTeams.membership(impostor)).toThrow(expect.objectContaining({ code: 'TEAM_NOT_MEMBER' }))
|
||||
|
||||
const orphanRoot = await ctx.agents.create({
|
||||
sessionId: SessionId('orphan-ordinary-root'),
|
||||
meta: { parentSession: SessionId('absent-parent') },
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
expect(ctx.teams.membership(orphanRoot.agent)).toMatchObject({ role: 'lead', name: 'lead' })
|
||||
expect(ctx.agentTeams.membership(orphanRoot.agent)).toMatchObject({ role: 'lead', name: 'lead' })
|
||||
await orphanRoot.dispose()
|
||||
})
|
||||
|
||||
@@ -490,7 +490,7 @@ describe('Team identity and provisioning', () => {
|
||||
resumeSessionId: started.childId,
|
||||
agentOptions: { provider: 'mock', model: 'mock' },
|
||||
})
|
||||
expect(first.ctx.teams.tryMembership(orphan.agent)).toBeUndefined()
|
||||
expect(first.ctx.agentTeams.tryMembership(orphan.agent)).toBeUndefined()
|
||||
expect(teamInternals(first.ctx).roster.liveChildrenByRoot()).toEqual(new Map())
|
||||
await orphan.dispose()
|
||||
|
||||
@@ -503,7 +503,7 @@ describe('Team identity and provisioning', () => {
|
||||
const journal = teamInternals(second.ctx).journal
|
||||
const state = journal.state.bind(journal)
|
||||
journal.state = () => { throw new Error('malformed Team stream') }
|
||||
expect(second.ctx.teams.tryMembership(child.agent)).toBeUndefined()
|
||||
expect(second.ctx.agentTeams.tryMembership(child.agent)).toBeUndefined()
|
||||
journal.state = state
|
||||
await child.dispose()
|
||||
})
|
||||
@@ -528,7 +528,7 @@ describe('Team shared task DAG', () => {
|
||||
})
|
||||
await ctx.sessions.flush(lead.session)
|
||||
|
||||
await expect(ctx.teams.createTask(lead, {
|
||||
await expect(ctx.agentTeams.createTask(lead, {
|
||||
subject: 'cannot allocate',
|
||||
description: 'no safe numeric task id remains',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_LIMIT' })
|
||||
@@ -536,20 +536,20 @@ describe('Team shared task DAG', () => {
|
||||
|
||||
it('bounds non-deleted tasks while retaining deleted task ids as tombstones', async () => {
|
||||
const { ctx, lead } = await setup([], { maxTasks: 1 })
|
||||
const first = await ctx.teams.createTask(lead, { subject: 'first', description: 'first task' })
|
||||
await expect(ctx.teams.createTask(lead, { subject: 'overflow', description: 'overflow task' }))
|
||||
const first = await ctx.agentTeams.createTask(lead, { subject: 'first', description: 'first task' })
|
||||
await expect(ctx.agentTeams.createTask(lead, { subject: 'overflow', description: 'overflow task' }))
|
||||
.rejects.toMatchObject({ code: 'TEAM_TASK_LIMIT' })
|
||||
|
||||
const deleted = await ctx.teams.updateTask(lead, {
|
||||
const deleted = await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: first.id,
|
||||
expectedRevision: first.revision,
|
||||
action: 'delete',
|
||||
})
|
||||
const second = await ctx.teams.createTask(lead, { subject: 'second', description: 'second task' })
|
||||
const second = await ctx.agentTeams.createTask(lead, { subject: 'second', description: 'second task' })
|
||||
expect(deleted.status).toBe('deleted')
|
||||
expect(second.id).toBe(TeamTaskId('task-2'))
|
||||
expect(ctx.teams.getTask(lead, first.id).status).toBe('deleted')
|
||||
expect(ctx.teams.listTasks(lead).map(task => task.id)).toEqual([second.id])
|
||||
expect(ctx.agentTeams.getTask(lead, first.id).status).toBe('deleted')
|
||||
expect(ctx.agentTeams.listTasks(lead).map(task => task.id)).toEqual([second.id])
|
||||
})
|
||||
|
||||
it('enforces CAS, ownership, dependencies, transitions, and write-scope warnings', async () => {
|
||||
@@ -559,63 +559,63 @@ describe('Team shared task DAG', () => {
|
||||
const secondMember = await spawn(ctx, lead, 'beta')
|
||||
const beta = await waitRunning(ctx, secondMember.member.id)
|
||||
|
||||
const first = await ctx.teams.createTask(alpha, {
|
||||
const first = await ctx.agentTeams.createTask(alpha, {
|
||||
subject: 'first',
|
||||
description: 'first task',
|
||||
writeScopes: ['src', './src/', 'src'],
|
||||
})
|
||||
const second = await ctx.teams.createTask(beta, {
|
||||
const second = await ctx.agentTeams.createTask(beta, {
|
||||
subject: 'second',
|
||||
description: 'second task',
|
||||
blockedBy: [first.id],
|
||||
writeScopes: ['src/feature'],
|
||||
})
|
||||
expect(first.writeScopes).toEqual(['src'])
|
||||
await expect(ctx.teams.updateTask(beta, {
|
||||
await expect(ctx.agentTeams.updateTask(beta, {
|
||||
taskId: second.id,
|
||||
expectedRevision: second.revision,
|
||||
action: 'claim',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_BLOCKED' })
|
||||
|
||||
const claimed = await ctx.teams.updateTask(alpha, {
|
||||
const claimed = await ctx.agentTeams.updateTask(alpha, {
|
||||
taskId: first.id,
|
||||
expectedRevision: first.revision,
|
||||
action: 'claim',
|
||||
})
|
||||
await expect(ctx.teams.updateTask(beta, {
|
||||
await expect(ctx.agentTeams.updateTask(beta, {
|
||||
taskId: first.id,
|
||||
expectedRevision: claimed.revision,
|
||||
action: 'claim',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_ALREADY_CLAIMED' })
|
||||
expect(ctx.teams.getTask(beta, second.id)).toMatchObject({
|
||||
expect(ctx.agentTeams.getTask(beta, second.id)).toMatchObject({
|
||||
ready: false,
|
||||
writeScopeWarnings: [`write scopes overlap with ${first.id}`],
|
||||
})
|
||||
await expect(ctx.teams.updateTask(beta, {
|
||||
await expect(ctx.agentTeams.updateTask(beta, {
|
||||
taskId: first.id,
|
||||
expectedRevision: claimed.revision,
|
||||
action: 'edit',
|
||||
subject: 'stolen',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_UNAUTHORIZED' })
|
||||
await expect(ctx.teams.updateTask(alpha, {
|
||||
await expect(ctx.agentTeams.updateTask(alpha, {
|
||||
taskId: first.id,
|
||||
expectedRevision: first.revision,
|
||||
action: 'complete',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_STALE_REVISION' })
|
||||
|
||||
const completed = await ctx.teams.updateTask(alpha, {
|
||||
const completed = await ctx.agentTeams.updateTask(alpha, {
|
||||
taskId: first.id,
|
||||
expectedRevision: claimed.revision,
|
||||
action: 'complete',
|
||||
})
|
||||
expect(completed.status).toBe('completed')
|
||||
expect(ctx.teams.getTask(beta, second.id).ready).toBe(true)
|
||||
const secondClaim = await ctx.teams.updateTask(beta, {
|
||||
expect(ctx.agentTeams.getTask(beta, second.id).ready).toBe(true)
|
||||
const secondClaim = await ctx.agentTeams.updateTask(beta, {
|
||||
taskId: second.id,
|
||||
expectedRevision: second.revision,
|
||||
action: 'claim',
|
||||
})
|
||||
const released = await ctx.teams.updateTask(beta, {
|
||||
const released = await ctx.agentTeams.updateTask(beta, {
|
||||
taskId: second.id,
|
||||
expectedRevision: secondClaim.revision,
|
||||
action: 'release',
|
||||
@@ -623,40 +623,40 @@ describe('Team shared task DAG', () => {
|
||||
expect(released).toMatchObject({ status: 'pending', ready: true })
|
||||
expect('ownerId' in released).toBe(false)
|
||||
|
||||
ctx.teams.interrupt(lead, 'alpha')
|
||||
ctx.teams.interrupt(lead, 'beta')
|
||||
ctx.agentTeams.interrupt(lead, 'alpha')
|
||||
ctx.agentTeams.interrupt(lead, 'beta')
|
||||
await Promise.all([waitNoAgent(ctx, alpha.id), waitNoAgent(ctx, beta.id)])
|
||||
})
|
||||
|
||||
it('rejects malformed scopes and every invalid dependency relation', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
const first = await ctx.teams.createTask(lead, { subject: 'one', description: 'one' })
|
||||
const second = await ctx.teams.createTask(lead, {
|
||||
const first = await ctx.agentTeams.createTask(lead, { subject: 'one', description: 'one' })
|
||||
const second = await ctx.agentTeams.createTask(lead, {
|
||||
subject: 'two', description: 'two', blockedBy: [first.id],
|
||||
})
|
||||
await expect(ctx.teams.createTask(lead, {
|
||||
await expect(ctx.agentTeams.createTask(lead, {
|
||||
subject: 'bad', description: 'bad', blockedBy: [TeamTaskId('missing')],
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_NOT_FOUND' })
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: first.id,
|
||||
expectedRevision: first.revision,
|
||||
action: 'set_dependencies',
|
||||
blockedBy: [second.id],
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_DEPENDENCY_CYCLE' })
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: first.id,
|
||||
expectedRevision: first.revision,
|
||||
action: 'set_dependencies',
|
||||
blockedBy: [first.id],
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_DEPENDENCY_CYCLE' })
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: first.id,
|
||||
expectedRevision: first.revision,
|
||||
action: 'set_dependencies',
|
||||
blockedBy: [second.id, second.id],
|
||||
})).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
for (const scope of ['', '.', '..', '/root', 'C:\\root', 'C:root', 'a//b', 'a/../b']) {
|
||||
await expect(ctx.teams.createTask(lead, {
|
||||
await expect(ctx.agentTeams.createTask(lead, {
|
||||
subject: 'scope', description: 'scope', writeScopes: [scope],
|
||||
})).rejects.toMatchObject({ code: 'TEAM_INVALID_WRITE_SCOPE' })
|
||||
}
|
||||
@@ -664,36 +664,36 @@ describe('Team shared task DAG', () => {
|
||||
|
||||
it('rejects incomplete mutations, invalid transitions, and deletion of a live blocker', async () => {
|
||||
const { ctx, lead } = await setup([])
|
||||
await expect(ctx.teams.createTask(lead, { subject: ' ', description: 'invalid' }))
|
||||
await expect(ctx.agentTeams.createTask(lead, { subject: ' ', description: 'invalid' }))
|
||||
.rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
await expect(ctx.teams.createTask(lead, { subject: 'invalid', description: '' }))
|
||||
await expect(ctx.agentTeams.createTask(lead, { subject: 'invalid', description: '' }))
|
||||
.rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
await expect(ctx.teams.createTask(lead, { subject: 'x'.repeat(201), description: 'too long' }))
|
||||
await expect(ctx.agentTeams.createTask(lead, { subject: 'x'.repeat(201), description: 'too long' }))
|
||||
.rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
const blocker = await ctx.teams.createTask(lead, { subject: 'blocker', description: 'blocker' })
|
||||
await ctx.teams.createTask(lead, {
|
||||
const blocker = await ctx.agentTeams.createTask(lead, { subject: 'blocker', description: 'blocker' })
|
||||
await ctx.agentTeams.createTask(lead, {
|
||||
subject: 'dependent', description: 'dependent', blockedBy: [blocker.id],
|
||||
})
|
||||
expect(() => ctx.teams.getTask(lead, TeamTaskId('missing')))
|
||||
expect(() => ctx.agentTeams.getTask(lead, TeamTaskId('missing')))
|
||||
.toThrow(expect.objectContaining({ code: 'TEAM_TASK_NOT_FOUND' }))
|
||||
for (const action of ['release', 'complete', 'reopen'] as const) {
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: blocker.id,
|
||||
expectedRevision: blocker.revision,
|
||||
action,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_INVALID_TRANSITION' })
|
||||
}
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: blocker.id,
|
||||
expectedRevision: blocker.revision,
|
||||
action: 'edit',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: blocker.id,
|
||||
expectedRevision: blocker.revision,
|
||||
action: 'set_dependencies',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_INVALID_ARGUMENT' })
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: blocker.id,
|
||||
expectedRevision: blocker.revision,
|
||||
action: 'delete',
|
||||
@@ -704,54 +704,54 @@ describe('Team shared task DAG', () => {
|
||||
const { ctx, lead } = await setup(['hang'])
|
||||
const started = await spawn(ctx, lead, 'owner')
|
||||
const owner = await waitRunning(ctx, started.member.id)
|
||||
const task = await ctx.teams.createTask(owner, { subject: 'lifecycle', description: 'lifecycle' })
|
||||
const assigned = await ctx.teams.updateTask(lead, {
|
||||
const task = await ctx.agentTeams.createTask(owner, { subject: 'lifecycle', description: 'lifecycle' })
|
||||
const assigned = await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: task.id,
|
||||
expectedRevision: task.revision,
|
||||
action: 'reassign',
|
||||
owner: 'owner',
|
||||
})
|
||||
await expect(ctx.teams.updateTask(owner, {
|
||||
await expect(ctx.agentTeams.updateTask(owner, {
|
||||
taskId: task.id,
|
||||
expectedRevision: assigned.revision,
|
||||
action: 'reassign',
|
||||
owner: 'lead',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_LEAD_REQUIRED' })
|
||||
const complete = await ctx.teams.updateTask(owner, {
|
||||
const complete = await ctx.agentTeams.updateTask(owner, {
|
||||
taskId: task.id,
|
||||
expectedRevision: assigned.revision,
|
||||
action: 'complete',
|
||||
})
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: task.id,
|
||||
expectedRevision: complete.revision,
|
||||
action: 'reassign',
|
||||
owner: 'lead',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_INVALID_TRANSITION' })
|
||||
const reopened = await ctx.teams.updateTask(owner, {
|
||||
const reopened = await ctx.agentTeams.updateTask(owner, {
|
||||
taskId: task.id,
|
||||
expectedRevision: complete.revision,
|
||||
action: 'reopen',
|
||||
})
|
||||
const claimed = await ctx.teams.updateTask(owner, {
|
||||
const claimed = await ctx.agentTeams.updateTask(owner, {
|
||||
taskId: task.id,
|
||||
expectedRevision: reopened.revision,
|
||||
action: 'claim',
|
||||
})
|
||||
const deleted = await ctx.teams.updateTask(owner, {
|
||||
const deleted = await ctx.agentTeams.updateTask(owner, {
|
||||
taskId: task.id,
|
||||
expectedRevision: claimed.revision,
|
||||
action: 'delete',
|
||||
})
|
||||
expect(deleted.status).toBe('deleted')
|
||||
expect(ctx.teams.listTasks(lead)).toEqual([])
|
||||
await expect(ctx.teams.updateTask(owner, {
|
||||
expect(ctx.agentTeams.listTasks(lead)).toEqual([])
|
||||
await expect(ctx.agentTeams.updateTask(owner, {
|
||||
taskId: task.id,
|
||||
expectedRevision: deleted.revision,
|
||||
action: 'edit',
|
||||
subject: 'late',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_DELETED' })
|
||||
ctx.teams.interrupt(lead, 'owner')
|
||||
ctx.agentTeams.interrupt(lead, 'owner')
|
||||
await waitNoAgent(ctx, owner.id)
|
||||
})
|
||||
|
||||
@@ -759,40 +759,40 @@ describe('Team shared task DAG', () => {
|
||||
const { ctx, lead } = await setup(['hang'])
|
||||
const started = await spawn(ctx, lead, 'editor')
|
||||
const editor = await waitRunning(ctx, started.member.id)
|
||||
const blocker = await ctx.teams.createTask(lead, { subject: 'blocker', description: 'blocker' })
|
||||
const task = await ctx.teams.createTask(lead, {
|
||||
const blocker = await ctx.agentTeams.createTask(lead, { subject: 'blocker', description: 'blocker' })
|
||||
const task = await ctx.agentTeams.createTask(lead, {
|
||||
subject: 'draft',
|
||||
description: 'draft description',
|
||||
blockedBy: [blocker.id],
|
||||
})
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: TeamTaskId('missing-update'), expectedRevision: 1, action: 'delete',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_NOT_FOUND' })
|
||||
await expect(ctx.teams.updateTask(lead, {
|
||||
await expect(ctx.agentTeams.updateTask(lead, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'reassign', owner: 'editor',
|
||||
})).rejects.toMatchObject({ code: 'TEAM_TASK_BLOCKED' })
|
||||
|
||||
const leadClaim = await ctx.teams.updateTask(lead, {
|
||||
const leadClaim = await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: blocker.id, expectedRevision: blocker.revision, action: 'claim',
|
||||
})
|
||||
expect(leadClaim.ownerName).toBe('lead')
|
||||
const completedBlocker = await ctx.teams.updateTask(lead, {
|
||||
const completedBlocker = await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: blocker.id, expectedRevision: leadClaim.revision, action: 'complete',
|
||||
})
|
||||
expect(completedBlocker.status).toBe('completed')
|
||||
const assigned = await ctx.teams.updateTask(lead, {
|
||||
const assigned = await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: task.id, expectedRevision: task.revision, action: 'reassign', owner: 'editor',
|
||||
})
|
||||
const subject = await ctx.teams.updateTask(editor, {
|
||||
const subject = await ctx.agentTeams.updateTask(editor, {
|
||||
taskId: task.id, expectedRevision: assigned.revision, action: 'edit', subject: 'edited subject',
|
||||
})
|
||||
const description = await ctx.teams.updateTask(editor, {
|
||||
const description = await ctx.agentTeams.updateTask(editor, {
|
||||
taskId: task.id,
|
||||
expectedRevision: subject.revision,
|
||||
action: 'edit',
|
||||
description: 'edited description',
|
||||
})
|
||||
const scopes = await ctx.teams.updateTask(editor, {
|
||||
const scopes = await ctx.agentTeams.updateTask(editor, {
|
||||
taskId: task.id,
|
||||
expectedRevision: description.revision,
|
||||
action: 'edit',
|
||||
@@ -803,34 +803,34 @@ describe('Team shared task DAG', () => {
|
||||
description: 'edited description',
|
||||
writeScopes: ['src/nested'],
|
||||
})
|
||||
const unassigned = await ctx.teams.updateTask(lead, {
|
||||
const unassigned = await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: task.id, expectedRevision: scopes.revision, action: 'reassign', owner: ' ',
|
||||
})
|
||||
expect(unassigned).toMatchObject({ status: 'pending' })
|
||||
expect('ownerId' in unassigned).toBe(false)
|
||||
|
||||
const broad = await ctx.teams.createTask(lead, {
|
||||
const broad = await ctx.agentTeams.createTask(lead, {
|
||||
subject: 'broad scope', description: 'broad scope', writeScopes: ['src'],
|
||||
})
|
||||
const narrow = await ctx.teams.createTask(lead, {
|
||||
const narrow = await ctx.agentTeams.createTask(lead, {
|
||||
subject: 'narrow scope', description: 'narrow scope', writeScopes: ['src/nested'],
|
||||
})
|
||||
const disjoint = await ctx.teams.createTask(lead, {
|
||||
const disjoint = await ctx.agentTeams.createTask(lead, {
|
||||
subject: 'disjoint scope', description: 'disjoint scope', writeScopes: ['docs'],
|
||||
})
|
||||
await ctx.teams.updateTask(lead, {
|
||||
await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: broad.id, expectedRevision: broad.revision, action: 'claim',
|
||||
})
|
||||
await ctx.teams.updateTask(lead, {
|
||||
await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: narrow.id, expectedRevision: narrow.revision, action: 'claim',
|
||||
})
|
||||
await ctx.teams.updateTask(lead, {
|
||||
await ctx.agentTeams.updateTask(lead, {
|
||||
taskId: disjoint.id, expectedRevision: disjoint.revision, action: 'claim',
|
||||
})
|
||||
expect(ctx.teams.getTask(lead, broad.id).writeScopeWarnings)
|
||||
expect(ctx.agentTeams.getTask(lead, broad.id).writeScopeWarnings)
|
||||
.toEqual([`write scopes overlap with ${narrow.id}`])
|
||||
|
||||
ctx.teams.interrupt(lead, 'editor')
|
||||
ctx.agentTeams.interrupt(lead, 'editor')
|
||||
await waitNoAgent(ctx, editor.id)
|
||||
})
|
||||
})
|
||||
@@ -843,10 +843,10 @@ describe('Team mailbox and waiting', () => {
|
||||
lead.followup(createUserMessage({ content: content('keep the Lead busy'), source: { kind: 'user' } }))
|
||||
await waitRunning(ctx, lead.id)
|
||||
|
||||
const first = await ctx.teams.sendMessage(reporter, {
|
||||
const first = await ctx.agentTeams.sendMessage(reporter, {
|
||||
target: 'lead', content: content('first wakeup report'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
const second = await ctx.teams.sendMessage(reporter, {
|
||||
const second = await ctx.agentTeams.sendMessage(reporter, {
|
||||
target: 'lead', content: content('second wakeup report'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
expect([first.status, second.status]).toEqual(['accepted', 'accepted'])
|
||||
@@ -890,7 +890,7 @@ describe('Team mailbox and waiting', () => {
|
||||
const { ctx, lead } = await setup(['hang'])
|
||||
const started = await spawn(ctx, lead, 'pending-target')
|
||||
const target = await waitRunning(ctx, started.member.id)
|
||||
const immediate = await ctx.teams.sendMessage(lead, {
|
||||
const immediate = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'pending-target',
|
||||
content: content('live quiet receipt'),
|
||||
delivery: 'quiet',
|
||||
@@ -969,7 +969,7 @@ describe('Team mailbox and waiting', () => {
|
||||
await expect(teamInternals(ctx).mailbox.tryDispatch(lead, disappearing, SIGNAL)).resolves.toBe(false)
|
||||
expect(durable(lead).pendingMessages.map(pending => pending.id)).toEqual([disappearing.id])
|
||||
|
||||
ctx.teams.interrupt(lead, 'pending-target')
|
||||
ctx.agentTeams.interrupt(lead, 'pending-target')
|
||||
target.cancel({ kind: 'parent' })
|
||||
await waitNoAgent(ctx, target.id)
|
||||
})
|
||||
@@ -985,7 +985,7 @@ describe('Team mailbox and waiting', () => {
|
||||
return flush(session)
|
||||
})
|
||||
|
||||
const first = await ctx.teams.sendMessage(lead, {
|
||||
const first = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'busy-target', content: content('first waking message'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
|
||||
@@ -996,7 +996,7 @@ describe('Team mailbox and waiting', () => {
|
||||
&& message.source.messageId === first.messageId)).toBe(true)
|
||||
|
||||
flushed.length = 0
|
||||
const second = await ctx.teams.sendMessage(lead, {
|
||||
const second = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'busy-target', content: content('second waking message'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
|
||||
@@ -1007,7 +1007,7 @@ describe('Team mailbox and waiting', () => {
|
||||
&& (message.source.messageId === first.messageId || message.source.messageId === second.messageId)))
|
||||
.toHaveLength(2)
|
||||
|
||||
ctx.teams.interrupt(lead, 'busy-target')
|
||||
ctx.agentTeams.interrupt(lead, 'busy-target')
|
||||
target.cancel({ kind: 'parent' })
|
||||
await waitNoAgent(ctx, target.id)
|
||||
})
|
||||
@@ -1030,12 +1030,12 @@ describe('Team mailbox and waiting', () => {
|
||||
return createUserMessage({ content: blocks, source: { kind: 'user' } }).id
|
||||
})
|
||||
|
||||
const first = ctx.teams.sendMessage(lead, {
|
||||
const first = ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'ordered-target', content: content('first waking'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
await entered.promise
|
||||
let secondSettled = false
|
||||
const second = ctx.teams.sendMessage(lead, {
|
||||
const second = ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'ordered-target', content: content('second waking'), delivery: 'wakeup', signal: SIGNAL,
|
||||
}).finally(() => { secondSettled = true })
|
||||
await new Promise<void>((resolve) => { setTimeout(resolve, 0) })
|
||||
@@ -1135,21 +1135,21 @@ describe('Team mailbox and waiting', () => {
|
||||
const inactiveStarted = await spawn(ctx, lead, 'inactive-target')
|
||||
await waitNoAgent(ctx, inactiveStarted.member.id)
|
||||
const inspect = vi.spyOn(ctx.sessionPersistence, 'inspect').mockRejectedValueOnce(new Error('inspect unavailable'))
|
||||
const uncertain = await ctx.teams.sendMessage(lead, {
|
||||
const uncertain = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'inactive-target', content: content('inspection failure'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
expect(uncertain.status).toBe('queued')
|
||||
inspect.mockRestore()
|
||||
|
||||
vi.spyOn(ctx.subagents, 'followup').mockRejectedValueOnce(new Error('delivery unavailable'))
|
||||
const failed = await ctx.teams.sendMessage(lead, {
|
||||
const failed = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'inactive-target', content: content('delivery failure'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
expect(failed.status).toBe('queued')
|
||||
expect(warnings.some(warning => warning.includes('inspect unavailable'))).toBe(true)
|
||||
expect(warnings.some(warning => warning.includes('delivery unavailable'))).toBe(true)
|
||||
|
||||
ctx.teams.interrupt(lead, 'live-target')
|
||||
ctx.agentTeams.interrupt(lead, 'live-target')
|
||||
await waitNoAgent(ctx, live.id)
|
||||
})
|
||||
|
||||
@@ -1160,12 +1160,12 @@ describe('Team mailbox and waiting', () => {
|
||||
const betaStarted = await spawn(ctx, lead, 'beta')
|
||||
await waitNoAgent(ctx, betaStarted.member.id)
|
||||
|
||||
const quiet = await ctx.teams.sendMessage(alpha, {
|
||||
const quiet = await ctx.agentTeams.sendMessage(alpha, {
|
||||
target: 'beta', content: content('quiet info'), delivery: 'quiet', signal: SIGNAL,
|
||||
})
|
||||
expect(quiet.status).toBe('queued')
|
||||
expect(ctx.agents.get(betaStarted.member.id)).toBeUndefined()
|
||||
const waking = await ctx.teams.sendMessage(alpha, {
|
||||
const waking = await ctx.agentTeams.sendMessage(alpha, {
|
||||
target: 'beta', content: content('do another turn'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
expect(waking.status).toBe('accepted')
|
||||
@@ -1193,7 +1193,7 @@ describe('Team mailbox and waiting', () => {
|
||||
[waking.messageId, 'alpha'],
|
||||
])
|
||||
|
||||
ctx.teams.interrupt(lead, 'alpha')
|
||||
ctx.agentTeams.interrupt(lead, 'alpha')
|
||||
await waitNoAgent(ctx, alpha.id)
|
||||
})
|
||||
|
||||
@@ -1204,25 +1204,25 @@ describe('Team mailbox and waiting', () => {
|
||||
})
|
||||
const target = await spawn(ctx, lead, 'target')
|
||||
await waitNoAgent(ctx, target.member.id)
|
||||
await expect(ctx.teams.sendMessage(lead, {
|
||||
await expect(ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'target', content: content('x'.repeat(300)), delivery: 'quiet', signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_MESSAGE_TOO_LARGE' })
|
||||
const queued = await ctx.teams.sendMessage(lead, {
|
||||
const queued = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'target', content: content('one'), delivery: 'quiet', signal: SIGNAL,
|
||||
})
|
||||
expect(queued.status).toBe('queued')
|
||||
await expect(ctx.teams.sendMessage(lead, {
|
||||
await expect(ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'target', content: content('two'), delivery: 'quiet', signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_MAILBOX_FULL' })
|
||||
await expect(ctx.teams.sendMessage(lead, {
|
||||
await expect(ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'lead', content: content('self'), delivery: 'quiet', signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_SELF_MESSAGE' })
|
||||
await expect(ctx.teams.sendMessage(lead, {
|
||||
await expect(ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'missing', content: content('unknown target'), delivery: 'quiet', signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_MEMBER_NOT_FOUND' })
|
||||
const controller = new AbortController()
|
||||
controller.abort(new TeamError('cancelled before queue', 'TEST_CANCELLED'))
|
||||
await expect(ctx.teams.sendMessage(lead, {
|
||||
await expect(ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'target', content: content('cancelled'), delivery: 'quiet', signal: controller.signal,
|
||||
})).rejects.toMatchObject({ code: 'TEST_CANCELLED' })
|
||||
})
|
||||
@@ -1231,11 +1231,11 @@ describe('Team mailbox and waiting', () => {
|
||||
const { ctx, lead } = await setup(['hang', textResponse('after interrupt')])
|
||||
const started = await spawn(ctx, lead, 'worker')
|
||||
const worker = await waitRunning(ctx, started.member.id)
|
||||
const followup = await ctx.teams.sendMessage(lead, {
|
||||
const followup = await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'worker', content: content('retained follow-up'), delivery: 'wakeup', signal: SIGNAL,
|
||||
})
|
||||
expect(followup.status).toBe('accepted')
|
||||
expect(ctx.teams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' })
|
||||
expect(ctx.agentTeams.interrupt(lead, 'worker')).toEqual({ previousStatus: 'running' })
|
||||
await vi.waitFor(() => { expect(worker.status).toBe('idle') })
|
||||
expect(worker.inbox.nextTurn.some(message => message.source.kind === 'team-message'
|
||||
&& message.source.messageId === followup.messageId)).toBe(true)
|
||||
@@ -1252,7 +1252,7 @@ describe('Team mailbox and waiting', () => {
|
||||
await ctx.plugin(AgentLoop, { agents: [] })
|
||||
await ctx.plugin(SubagentService)
|
||||
const fiber = await ctx.plugin(TeamService)
|
||||
const service = ctx.teams
|
||||
const service = ctx.agentTeams
|
||||
const lead = ctx.agentLoop.create(SessionId('wait-lead'), {})
|
||||
|
||||
await expect(service.waitForChange(lead, 9_999, SIGNAL))
|
||||
@@ -1314,25 +1314,25 @@ describe('Team mailbox and waiting', () => {
|
||||
const disposed = service.waitForChange(lead, 10_000, SIGNAL)
|
||||
await fiber.dispose()
|
||||
await expect(disposed).resolves.toEqual({ timedOut: false })
|
||||
expect(ctx.get('teams')).toBeUndefined()
|
||||
expect(ctx.get('agentTeams')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('disposes live teammate Activations and their waits when the Team service unloads', async () => {
|
||||
const { ctx, lead, teamFiber } = await setup(['hang'])
|
||||
const started = await spawn(ctx, lead, 'dispose-worker')
|
||||
await waitRunning(ctx, started.member.id)
|
||||
const waiting = ctx.teams.waitForChange(lead, 10_000, SIGNAL)
|
||||
const waiting = ctx.agentTeams.waitForChange(lead, 10_000, SIGNAL)
|
||||
|
||||
await teamFiber.dispose()
|
||||
|
||||
await expect(waiting).resolves.toEqual({ timedOut: false })
|
||||
expect(ctx.agents.get(started.member.id)).toBeUndefined()
|
||||
expect(ctx.get('teams')).toBeUndefined()
|
||||
expect(ctx.get('agentTeams')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('closes creation admission and drains an in-flight spawn before unload completes', async () => {
|
||||
const { ctx, lead, teamFiber } = await setup(['hang'])
|
||||
const service = ctx.teams
|
||||
const service = ctx.agentTeams
|
||||
const start = ctx.subagents.startContinuable.bind(ctx.subagents)
|
||||
const entered = Promise.withResolvers<undefined>()
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
@@ -1363,7 +1363,7 @@ describe('Team mailbox and waiting', () => {
|
||||
await rejected
|
||||
await disposal
|
||||
if (childId !== undefined) expect(ctx.agents.get(childId)).toBeUndefined()
|
||||
expect(ctx.get('teams')).toBeUndefined()
|
||||
expect(ctx.get('agentTeams')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('retains an in-flight creation cleanup failure during disposal', async () => {
|
||||
@@ -1433,9 +1433,9 @@ describe('Team mailbox and waiting', () => {
|
||||
},
|
||||
})
|
||||
await ctx.sessions.flush(lead.session)
|
||||
expect(ctx.teams.listMembers(lead)[1]?.status).toBe('failed')
|
||||
expect(ctx.agentTeams.listMembers(lead)[1]?.status).toBe('failed')
|
||||
|
||||
const internal = ctx.teams as unknown as { disposeRuntime(): Promise<void> }
|
||||
const internal = ctx.agentTeams as unknown as { disposeRuntime(): Promise<void> }
|
||||
await internal.disposeRuntime()
|
||||
expect(ctx.agents.get(childId)).toBeUndefined()
|
||||
})
|
||||
@@ -1460,14 +1460,14 @@ describe('Team mailbox and waiting', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const sending = ctx.teams.sendMessage(lead, {
|
||||
const sending = ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'mailbox-worker',
|
||||
content: content('resume during disposal'),
|
||||
delivery: 'wakeup',
|
||||
signal: SIGNAL,
|
||||
})
|
||||
await entered.promise
|
||||
const internal = ctx.teams as unknown as { disposeRuntime(): Promise<void> }
|
||||
const internal = ctx.agentTeams as unknown as { disposeRuntime(): Promise<void> }
|
||||
let disposed = false
|
||||
const disposal = internal.disposeRuntime().then(() => { disposed = true })
|
||||
await aborted.promise
|
||||
@@ -1521,7 +1521,7 @@ describe('Team mailbox and waiting', () => {
|
||||
},
|
||||
}), { surfaceOp: 'append' })
|
||||
|
||||
const internal = ctx.teams as unknown as { disposeRuntime(): Promise<void> }
|
||||
const internal = ctx.agentTeams as unknown as { disposeRuntime(): Promise<void> }
|
||||
let disposed = false
|
||||
const disposal = internal.disposeRuntime().then(() => { disposed = true })
|
||||
await entered.promise
|
||||
@@ -1549,7 +1549,7 @@ describe('Team mailbox and waiting', () => {
|
||||
])
|
||||
expect(outcome).toBe('disposed')
|
||||
expect(drain).toHaveBeenCalledWith(lead, [started.member.id])
|
||||
expect(ctx.get('teams')).toBeUndefined()
|
||||
expect(ctx.get('agentTeams')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('bounds disposal while an admitted creation ignores cancellation', async () => {
|
||||
@@ -1558,7 +1558,7 @@ describe('Team mailbox and waiting', () => {
|
||||
internal.roster.inFlightCreations.add(new Promise(() => {}))
|
||||
|
||||
await expect(internal.disposeRuntime()).rejects.toBeInstanceOf(AggregateError)
|
||||
await expect(ctx.teams.spawnTeammate(lead, {
|
||||
await expect(ctx.agentTeams.spawnTeammate(lead, {
|
||||
name: 'after-timeout',
|
||||
description: 'admission remains closed',
|
||||
prompt: content('must reject'),
|
||||
@@ -1566,7 +1566,7 @@ describe('Team mailbox and waiting', () => {
|
||||
provider: 'spawn',
|
||||
signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_DISPOSED' })
|
||||
await expect(ctx.teams.sendMessage(lead, {
|
||||
await expect(ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'nobody', content: content('must reject'), delivery: 'quiet', signal: SIGNAL,
|
||||
})).rejects.toMatchObject({ code: 'TEAM_DISPOSED' })
|
||||
await expect(internal.mailbox.tryDispatch(lead, {
|
||||
@@ -1626,7 +1626,7 @@ describe('Team mailbox and waiting', () => {
|
||||
vi.spyOn(ctx.subagents, 'drainContinuableDescendants').mockRejectedValueOnce(new Error('drain failure'))
|
||||
|
||||
await teamFiber.dispose()
|
||||
expect(ctx.get('teams')).toBeUndefined()
|
||||
expect(ctx.get('agentTeams')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reconciles mismatched persisted children and ignores a concurrently settled member', async () => {
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/experimental/tool-agent-team/README.md
|
||||
README.md: c195f7e7fb977e81a507b8c1c58b1cc3ffaab230
|
||||
README.zh.md: 83c10a2d07ec6e0665c89adc69a8b2ccecdfc49a
|
||||
README.md: a0ff967199e8a22b60483aa59442a1cbefb261a8
|
||||
README.zh.md: 3dc4d9ea1631cc23e171d0689271f60ff84f92c4
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Scoped model-facing adapter for [`ctx.teams`](../agent-team/README.md). It installs the Agent Teams policy and collaboration tools in each implicit Lead and durable teammate scope. Scoped Team definitions shadow same-named legacy global continuable-subagent controls, so a composition that mounts both must disable the legacy definitions.
|
||||
Scoped model-facing adapter for [`ctx.agentTeams`](../agent-team/README.md). It installs the Agent Teams policy and collaboration tools in each implicit Lead and durable teammate scope. Scoped Team definitions shadow same-named legacy global continuable-subagent controls, so a composition that mounts both must disable the legacy definitions.
|
||||
|
||||
## Config
|
||||
|
||||
```yaml
|
||||
- id: tool-team
|
||||
- id: tool-agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-tool-agent-team'
|
||||
config:
|
||||
freshProvider: spawn
|
||||
@@ -20,7 +20,7 @@ Scoped model-facing adapter for [`ctx.teams`](../agent-team/README.md). It insta
|
||||
|
||||
The generated [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-agent-team) owns exact schemas. The adapter supplies teammate creation; quiet and waking peer delivery; roster listing, waiting, and Lead-only interruption; and task create/list/get/compare-and-set update operations.
|
||||
|
||||
Every tool requires the exact calling `Agent`. `spawn_teammate` and `interrupt_agent` enforce Lead authority inside `ctx.teams`, not only in their descriptions. All members can communicate with any peer and use the task board. Task mutations retain the domain's owner/Lead and revision checks.
|
||||
Every tool requires the exact calling `Agent`. `spawn_teammate` and `interrupt_agent` enforce Lead authority inside `ctx.agentTeams`, not only in their descriptions. All members can communicate with any peer and use the task board. Task mutations retain the domain's owner/Lead and revision checks.
|
||||
|
||||
`send_message` succeeds once mail is durable and never wakes an inactive target. `followup_task` also makes the message the target's next turn and can cold-resume it. A `queued` result is accepted durable work and must not be retried. Task readiness does not start an owner. Before arming its 10,000-through-3,600,000-millisecond edge wait, `wait_agent` checks for another member that is running or provisioning; without one it returns `noProgress` immediately with instructions to re-list and use `followup_task`. Otherwise it waits for one post-call Team edge, defaulting to 30,000 milliseconds, and callers re-list after wakeup or timeout because earlier changes are not replayed.
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
[`ctx.teams`](../agent-team/README.md) 的 scoped 模型适配器。它会在每个隐式 Lead 与持久 teammate scope 中安装 Agent Teams 策略和协作工具。scoped Team 定义会覆盖同名的旧全局 continuable-subagent control,因此同时挂载两者的组合必须禁用旧定义。
|
||||
[`ctx.agentTeams`](../agent-team/README.md) 的 scoped 模型适配器。它会在每个隐式 Lead 与持久 teammate scope 中安装 Agent Teams 策略和协作工具。scoped Team 定义会覆盖同名的旧全局 continuable-subagent control,因此同时挂载两者的组合必须禁用旧定义。
|
||||
|
||||
## 配置
|
||||
|
||||
```yaml
|
||||
- id: tool-team
|
||||
- id: tool-agent-team
|
||||
name: '@deepseek-ai/dsh-experimental-tool-agent-team'
|
||||
config:
|
||||
freshProvider: spawn
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
生成的[工具目录](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-agent-team)负责精确 schema。该适配器提供 teammate 创建;quiet 与 waking peer 投递;roster 列表、等待和仅限 Lead 的 interrupt;以及任务 create/list/get/CAS update 操作。
|
||||
|
||||
每个工具都要求完全相同的调用 `Agent`。`spawn_teammate` 与 `interrupt_agent` 在 `ctx.teams` 内部强制执行 Lead 权限,而不只依赖描述。所有成员都可以与任意 peer 通讯并使用任务板。任务变更保留领域层的 Owner/Lead 与 revision 校验。
|
||||
每个工具都要求完全相同的调用 `Agent`。`spawn_teammate` 与 `interrupt_agent` 在 `ctx.agentTeams` 内部强制执行 Lead 权限,而不只依赖描述。所有成员都可以与任意 peer 通讯并使用任务板。任务变更保留领域层的 Owner/Lead 与 revision 校验。
|
||||
|
||||
`send_message` 在 mail 持久化后即成功,并且绝不会唤醒 inactive target。`followup_task` 还会让该消息成为 target 的下一个 turn,并可冷恢复 target。`queued` 结果表示持久工作已经接受,不能重试。任务 ready 不会启动 owner。`wait_agent` 在注册 10,000 到 3,600,000 毫秒的边等待前,会检查是否有另一个 running 或 provisioning member;如果没有,它会立即返回 `noProgress`,提示重新 list 并使用 `followup_task`。否则它会等待调用后发生的一条 Team 边,默认 30,000 毫秒;由于不会回放更早的变化,调用方需要在唤醒或超时后重新 list。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-experimental-tool-agent-team",
|
||||
"description": "Scoped model-facing Agent Teams tools over ctx.teams",
|
||||
"description": "Scoped model-facing Agent Teams tools over ctx.agentTeams",
|
||||
"version": "0.1.0-rc.7",
|
||||
"private": true,
|
||||
"repository": {
|
||||
|
||||
@@ -9,9 +9,9 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { InferValue, ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
/** Cordis plugin name. */
|
||||
export const name = 'tool-team'
|
||||
export const name = 'tool-agent-team'
|
||||
/** Services required by the Team tool plugin. */
|
||||
export const inject = ['agents', 'teams', 'tools', 'systemPrompt']
|
||||
export const inject = ['agents', 'agentTeams', 'tools', 'systemPrompt']
|
||||
|
||||
/** Tool routing configuration. */
|
||||
export interface Config {
|
||||
@@ -165,7 +165,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
name: 'team:policy',
|
||||
order: 60,
|
||||
text: () => {
|
||||
const membership = ctx.teams.membership(agent)
|
||||
const membership = ctx.agentTeams.membership(agent)
|
||||
return `${POLICY}\n\nYour Team role is ${membership.role}; your Team name is ${membership.name}; Team id is ${membership.id}.`
|
||||
},
|
||||
}))
|
||||
@@ -187,7 +187,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
async execute(args, exec) {
|
||||
const agent = callingAgent(exec.agent, 'spawn_teammate')
|
||||
const context = args.context ?? 'fresh'
|
||||
return await ctx.teams.spawnTeammate(agent, {
|
||||
return await ctx.agentTeams.spawnTeammate(agent, {
|
||||
name: args.name,
|
||||
description: args.description,
|
||||
prompt: [{ type: 'text', text: args.prompt }],
|
||||
@@ -210,7 +210,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
},
|
||||
output: jsonOutput(SEND_VALUE_SCHEMA),
|
||||
execute(args, exec) {
|
||||
return ctx.teams.sendMessage(callingAgent(exec.agent, toolName), {
|
||||
return ctx.agentTeams.sendMessage(callingAgent(exec.agent, toolName), {
|
||||
target: args.target,
|
||||
content: [{ type: 'text', text: args.message }],
|
||||
delivery,
|
||||
@@ -228,7 +228,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
parameters: {},
|
||||
output: jsonOutput(MEMBER_LIST_VALUE_SCHEMA),
|
||||
async execute(_args, exec) {
|
||||
return Promise.resolve(ctx.teams.listMembers(callingAgent(exec.agent, 'list_agents')))
|
||||
return Promise.resolve(ctx.agentTeams.listMembers(callingAgent(exec.agent, 'list_agents')))
|
||||
},
|
||||
})))
|
||||
|
||||
@@ -248,11 +248,11 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
// Preserve TeamService's authoritative timeout validation before the
|
||||
// model-only no-progress shortcut.
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 10_000 || timeoutMs > 3_600_000) {
|
||||
return await ctx.teams.waitForChange(caller, timeoutMs, exec.signal)
|
||||
return await ctx.agentTeams.waitForChange(caller, timeoutMs, exec.signal)
|
||||
}
|
||||
// The active-peer read and waiter registration must remain one synchronous
|
||||
// span; awaiting between them can lose the only peer-status edge.
|
||||
const hasActivePeer = ctx.teams.listMembers(caller).some(member =>
|
||||
const hasActivePeer = ctx.agentTeams.listMembers(caller).some(member =>
|
||||
member.id !== caller.id && ACTIVE_WAIT_STATUSES.has(member.status))
|
||||
if (!hasActivePeer) {
|
||||
return {
|
||||
@@ -263,7 +263,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
},
|
||||
}
|
||||
}
|
||||
return await ctx.teams.waitForChange(caller, timeoutMs, exec.signal)
|
||||
return await ctx.agentTeams.waitForChange(caller, timeoutMs, exec.signal)
|
||||
},
|
||||
})))
|
||||
|
||||
@@ -275,7 +275,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
},
|
||||
output: jsonOutput(INTERRUPT_VALUE_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
return Promise.resolve(ctx.teams.interrupt(
|
||||
return Promise.resolve(ctx.agentTeams.interrupt(
|
||||
callingAgent(exec.agent, 'interrupt_agent'),
|
||||
args.target,
|
||||
))
|
||||
@@ -297,7 +297,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
},
|
||||
output: jsonOutput(TASK_VIEW_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
return await ctx.teams.createTask(callingAgent(exec.agent, 'team_task_create'), {
|
||||
return await ctx.agentTeams.createTask(callingAgent(exec.agent, 'team_task_create'), {
|
||||
subject: args.subject,
|
||||
description: args.description,
|
||||
...args.blocked_by === undefined ? {} : { blockedBy: args.blocked_by.map(TeamTaskId) },
|
||||
@@ -323,7 +323,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
output: jsonOutput(TASK_LIST_VALUE_SCHEMA),
|
||||
execute(args, exec) {
|
||||
const status = args.status
|
||||
const filtered = ctx.teams.listTasks(callingAgent(exec.agent, 'team_task_list')).filter(task =>
|
||||
const filtered = ctx.agentTeams.listTasks(callingAgent(exec.agent, 'team_task_list')).filter(task =>
|
||||
(status === undefined || task.status === status)
|
||||
&& (args.owner === undefined || (args.owner === 'unowned' ? task.ownerName === undefined : task.ownerName === args.owner))
|
||||
&& (args.ready === undefined || task.ready === args.ready))
|
||||
@@ -346,7 +346,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
},
|
||||
output: jsonOutput(TASK_VIEW_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
return Promise.resolve(ctx.teams.getTask(
|
||||
return Promise.resolve(ctx.agentTeams.getTask(
|
||||
callingAgent(exec.agent, 'team_task_get'),
|
||||
TeamTaskId(args.task_id),
|
||||
))
|
||||
@@ -373,7 +373,7 @@ function install(agent: Agent, ctx: Context, config: Required<Config>): () => vo
|
||||
},
|
||||
output: jsonOutput(TASK_VIEW_SCHEMA),
|
||||
async execute(args, exec) {
|
||||
return await ctx.teams.updateTask(callingAgent(exec.agent, 'team_task_update'), {
|
||||
return await ctx.agentTeams.updateTask(callingAgent(exec.agent, 'team_task_update'), {
|
||||
taskId: TeamTaskId(args.task_id),
|
||||
expectedRevision: args.expected_revision,
|
||||
action: args.action,
|
||||
@@ -402,7 +402,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
||||
}
|
||||
const installed = new Map<Agent, () => void>()
|
||||
const maybeInstall = (agent: Agent): void => {
|
||||
if (installed.has(agent) || ctx.teams.tryMembership(agent) === undefined) return
|
||||
if (installed.has(agent) || ctx.agentTeams.tryMembership(agent) === undefined) return
|
||||
installed.set(agent, install(agent, ctx, resolved))
|
||||
}
|
||||
for (const agent of ctx.agents.list()) maybeInstall(agent)
|
||||
|
||||
@@ -402,7 +402,7 @@ describe('dsh-tool-team', () => {
|
||||
const childId = spawnedChildId(spawned)
|
||||
await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 })
|
||||
|
||||
await ctx.teams.sendMessage(lead, {
|
||||
await ctx.agentTeams.sendMessage(lead, {
|
||||
target: 'cold-worker',
|
||||
content: [{ type: 'text', text: 'resume with Team scope' }],
|
||||
delivery: 'wakeup',
|
||||
@@ -422,8 +422,8 @@ describe('dsh-tool-team', () => {
|
||||
expect(result.isError).toBe(true)
|
||||
expect(text(result)).toContain('unknown tool "list_agents"')
|
||||
expect('default' in toolTeam).toBe(false)
|
||||
expect(toolTeam.name).toBe('tool-team')
|
||||
expect(toolTeam.inject).toEqual(['agents', 'teams', 'tools', 'systemPrompt'])
|
||||
expect(toolTeam.name).toBe('tool-agent-team')
|
||||
expect(toolTeam.inject).toEqual(['agents', 'agentTeams', 'tools', 'systemPrompt'])
|
||||
})
|
||||
|
||||
it('uses configured fresh and fork provider names', async () => {
|
||||
@@ -437,6 +437,6 @@ describe('dsh-tool-team', () => {
|
||||
expect(result.isError).toBe(false)
|
||||
const childId = spawnedChildId(result)
|
||||
await vi.waitFor(() => { expect(ctx.agents.get(childId)).toBeUndefined() }, { timeout: 5_000 })
|
||||
expect(ctx.teams.listMembers(lead)[1]).toMatchObject({ provider: 'team-fresh' })
|
||||
expect(ctx.agentTeams.listMembers(lead)[1]).toMatchObject({ provider: 'team-fresh' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,7 +98,7 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
systemPrompt: 'system-prompt.md',
|
||||
jobs: 'jobs.md',
|
||||
sessionTelemetry: 'session-telemetry.md',
|
||||
teams: 'team.md',
|
||||
agentTeams: 'agent-team.md',
|
||||
tokenMeter: 'token-meter.md',
|
||||
toolResultPruner: 'compaction.md',
|
||||
tools: 'tools.md',
|
||||
|
||||
@@ -479,12 +479,12 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
note: 'Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route.',
|
||||
},
|
||||
{
|
||||
key: 'teams',
|
||||
pkg: 'team',
|
||||
key: 'agentTeams',
|
||||
pkg: 'agent-team',
|
||||
title: 'Agent Teams coordination domain',
|
||||
mode: 'core',
|
||||
consumers: ['tool-team'],
|
||||
note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-team contributes the scoped model policy and controls.',
|
||||
consumers: ['tool-agent-team'],
|
||||
note: 'Owns the implicit-root roster, durable peer mailbox, shared task DAG, and continuable-child lifecycle; tool-agent-team contributes the scoped model policy and controls.',
|
||||
},
|
||||
{
|
||||
key: 'jobs',
|
||||
|
||||
@@ -527,7 +527,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-experimental-tool-agent-team',
|
||||
dir: 'tool-agent-team',
|
||||
source: 'packages/experimental/tool-agent-team/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.teams', 'an exact live Team member Agent'],
|
||||
requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.agentTeams', 'an exact live Team member Agent'],
|
||||
writes: ['tool/call', 'team/member', 'team/message/queued', 'team/message/delivered', 'team/task', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(AgentRegistry)
|
||||
@@ -540,7 +540,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
role: 'lead' as const,
|
||||
name: 'lead',
|
||||
}
|
||||
ctx.provide('teams', {
|
||||
ctx.provide('agentTeams', {
|
||||
tryMembership: (candidate: Agent) => candidate === agent ? membership : undefined,
|
||||
membership: () => membership,
|
||||
} as unknown as TeamService)
|
||||
@@ -553,7 +553,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
} as unknown as Agent
|
||||
Object.assign(agent, { ctx: createScope(inner, agent).ctx })
|
||||
inner.agents.register(agent)
|
||||
}, { inject: ['tools', 'systemPrompt', 'agents', 'teams'] }))
|
||||
}, { inject: ['tools', 'systemPrompt', 'agents', 'agentTeams'] }))
|
||||
await ctx.plugin(ToolTeam)
|
||||
catalogChildScopes.set(ctx, agent)
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user