Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ Defaults work without extra setup. A trusted profile can override member behavio

`slashCommand: false` disables the deterministic `/agent-teams` activation surfaces (slash command and gesture boundary), leaving the natural-language trigger as the only entry point.

The Plugins configuration tab exposes one AgentTeams checkbox, 支持工具并行 (Parallel Emission, settings namespace `agent-teams`, default off). When enabled, the captain usage section, newly spawned member personas, and assignment prompts allow several `agent_teams_*` tool calls in one response; same-response calls still run in order, and the Agent Loop parallel-cap setting is never touched.

## Boundaries

- One captain leads one active team at a time.
Expand Down
2 changes: 2 additions & 0 deletions README_ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ npm 默认标签 `latest` 现指向 `0.1.17`,因此新 profile 使用 `dsh plu

`slashCommand: false` 可关闭确定性的 `/agent-teams` 激活面(slash 命令与手势边界),仅保留自然语言触发。

Web 插件配置页的 AgentTeams 卡片提供唯一一个设置项「支持工具并行」(Parallel Emission,设置命名空间 `agent-teams`,默认关闭):开启后,队长 usage 提示段、新成员 persona 与任务派工提示词允许模型在同一条回复内发出多个 `agent_teams_*` 工具调用;同一回复内的调用仍按顺序执行,且不修改 Agent Loop 的并行上限。

## 使用边界

- 一个队长同一时间只能带一个活动团队。
Expand Down
4 changes: 4 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@

最终优先级为:成员显式 `provider` + `model` / `model` → `memberModel` → 队长当前路由。成员沿用队长当前 provider/model 时继承队长的思考强度;provider 或 model 任一改变时自动使用目标模型的默认档。显式 `reasoning_effort`(目标模型支持的档位 id,或 `"default"`)优先,并在目标 provider/model 上创建前校验;不兼容时成员创建会明确失败。最终生效的 provider/model/思考强度会写入 `team.json`,供状态查询和成员冷恢复使用。

### 支持工具并行(Parallel Emission)

Web 插件配置页的 AgentTeams 卡片提供唯一一个设置项「支持工具并行」(设置命名空间 `agent-teams` 下的 `parallelToolCalls`,默认关闭)。开启后,队长 usage 提示段(随插件挂载快照生效,切换后需重载插件/重启宿主)、新成员 persona(spawn 时定格,已创建成员不受影响)和任务派工提示词(每次派发实时读取)会允许模型在同一条回复内发出多个 `agent_teams_*` 工具调用。同一回复内的独占工具仍按顺序逐个执行,因此靠后的 `create_task` 可以引用同一回复中先前创建的任务 id;依赖 id 仍必须真实存在,不会接受前向引用。该开关不修改 Agent Loop 的并行上限(`maxParallelToolCalls`,独立设置)。

## 使用协议

插件提示段会指导模型按两阶段协议执行:继续已有团队,按需通过 `agent_teams_status` 确认状态 → 无团队时创建 staged 团队 → 写入可编辑成员占位 → 拆任务并声明依赖 → 等待用户审查 → **Approve & Run** 后原子创建成员并启动调度 → 队长监控/引导 → 汇报后 `agent_teams_delete`。staged 阶段没有子会话、不会领取任务。只有用户明确要求跳过审查时才使用 `approval: automatic`。成员之间可以直接互发消息,无需队长中转。驻留成员在中断或正常结束一轮后若仍持有 `claimed/in_progress` 任务,该 attempt 会停驻;只有显式重试/转派/接管才会撤销它。本进程已经观察过的停驻 attempt 在 Harness 回收其 AgentHandle 后仍保持原 attempt,Captain 轮询 `agent_teams_status` 不会因此重铸。只有冷启动或从未被本进程观察过的开放任务,才会自动恢复一次;恢复投递失败会回到原来的 capability,而不会变成可无限重派的 `pending`。
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,11 @@
"@deepseek-ai/dsh-commands": "0.1.5-rc.1",
"@deepseek-ai/dsh-llm": "0.1.5-rc.1",
"@deepseek-ai/dsh-llm-retry": "0.1.5-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
"@deepseek-ai/dsh-client-ui-settings-plugins": "0.1.5-rc.1",
"@deepseek-ai/dsh-session": "0.1.5-rc.1",
"@deepseek-ai/dsh-session-projection": "0.1.5-rc.1",
"@deepseek-ai/dsh-settings": "0.1.5-rc.1",
"@deepseek-ai/dsh-subagent": "0.1.5-rc.1",
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.1",
"@deepseek-ai/dsh-tools": "0.1.5-rc.1",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions scripts/lifecycle-verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,19 @@ try {
missingContractRejected = true
}
check('quality implementation without contract is rejected', missingContractRejected)
// Parallel Emission slice 1 (issue #62): same-response create_task may
// depend on earlier ids from the same response, but dependency ids must
// still EXIST — no forward references to ids that were never created.
let unknownDependencyRejected = false
try {
await call('agent_teams_create_task', {
subject: 'child before its parent exists',
dependencies: ['task-that-never-existed'],
})
} catch (error) {
unknownDependencyRejected = /does not exist/.test(String(error?.message ?? error))
}
check('create_task still rejects unknown dependency ids (no forward references)', unknownDependencyRejected)
const qualityTeam = await readTeam(stateRoot, 'quality-loop')
const builder = [...liveAgents.values()].find(agent => qualityTeam?.members.some(member => member.id === agent.id && member.name === 'builder'))
let criticMember
Expand Down
80 changes: 79 additions & 1 deletion scripts/verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ import { steerCaptainReport } from '../lib/tools.js'
import { parseProfileInvocation, resolveTeamProfile, formatProfilesForPrompt } from '../lib/profiles.js'
import { memberPersona, memberWelcome } from '../lib/members.js'
import { collectCompletedDependencyOutputs, formatDependencyOutputs, assignmentPrompt } from '../lib/scheduler.js'
import { AGENT_TEAMS_SETTINGS_NAMESPACE, AgentTeamsSettingsSchema } from '../lib/settings.js'
import { usageSectionText } from '../lib/index.js'
import {
installMemberSelectionRuntime,
resolveMemberLlmSelection,
Expand Down Expand Up @@ -143,6 +145,47 @@ const injected = 'The product interface should present the intended outcome, not
const assignment = assignmentPrompt({ taskId: 't1', memberName: 'Implementer', memberId: 'm', attempt: 1, attemptId: 'a', subject: 'x', dependencyOutputs: [], executionPrompt: injected }, '.agent-teams', 'demo')
check('execution prompt is injected into persona and assignment', assignment.includes(injected) && memberPersona({ name: 'Demo', id: 'demo', description: 'goal', captainSessionId: 'c', createdAt: 0, members: [], tasks: [], taskSeq: 0 }, { name: 'Implementer', id: 'm', role: 'builder', joinedAt: 0, status: 'idle', executionPrompt: injected }, '.agent-teams').includes(injected))

// Parallel Emission (issue #62 Slice 1): the three protocol surfaces branch on
// the boolean, and the settings namespace defaults to the serial protocol.
const settingsDefaults = AgentTeamsSettingsSchema({})
check(
'agent-teams settings namespace is the single Parallel Emission checkbox defaulting to false',
AGENT_TEAMS_SETTINGS_NAMESPACE === 'agent-teams'
&& AgentTeamsSettingsSchema({ parallelToolCalls: true }).parallelToolCalls === true
&& settingsDefaults.parallelToolCalls === false,
`resolved default = ${JSON.stringify(settingsDefaults)}`,
)
const personaOff = memberPersona({ name: 'Demo', id: 'demo', description: 'goal', captainSessionId: 'c', createdAt: 0, members: [], tasks: [], taskSeq: 0 }, { name: 'Implementer', id: 'm', role: 'builder', joinedAt: 0, status: 'idle' }, '.agent-teams')
const personaOn = memberPersona({ name: 'Demo', id: 'demo', description: 'goal', captainSessionId: 'c', createdAt: 0, members: [], tasks: [], taskSeq: 0 }, { name: 'Implementer', id: 'm', role: 'builder', joinedAt: 0, status: 'idle' }, '.agent-teams', undefined, true)
check(
'member persona keeps the serial protocol by default and allows same-response batching only when on',
!personaOff.includes('Parallel Emission')
&& personaOn.includes('Parallel Emission is enabled for you')
&& personaOn.includes('claim a task and mark it in_progress in the same response')
&& personaOn.includes('send your captain report with agent_teams_send_message in that same response'),
)
const assignmentOff = assignmentPrompt({ taskId: 't1', memberName: 'Implementer', memberId: 'm', attempt: 1, attemptId: 'a', subject: 'x', dependencyOutputs: [] }, '.agent-teams', 'demo')
const assignmentOn = assignmentPrompt({ taskId: 't1', memberName: 'Implementer', memberId: 'm', attempt: 1, attemptId: 'a', subject: 'x', dependencyOutputs: [] }, '.agent-teams', 'demo', true)
check(
'assignment prompt keeps the serial protocol by default and allows same-response batching only when on',
!assignmentOff.includes('Parallel Emission')
&& assignmentOn.includes('Parallel Emission is enabled')
&& assignmentOn.includes('claim this task and mark it in_progress in the same response')
&& assignmentOn.includes('send_message to the captain in that same response'),
)
const usageOff = usageSectionText('agent_teams_create_task', '')
const usageOn = usageSectionText('agent_teams_create_task', '', true)
check(
'captain usage section reads the live switch: serial by default, same-response creates when on',
!usageOff.includes('Parallel Emission is enabled')
&& usageOff.includes('one agent_teams_* tool call per response')
&& usageOff.includes('wait for its returned id')
&& usageOn.includes('Parallel Emission is enabled')
&& usageOn.includes('several agent_teams_* tool calls in one response')
&& usageOn.includes('same-response calls still run in order')
&& !usageOff.includes('Poll status until terminal'),
)


// The bundle patch's `name` is the specifier Node resolves when a profile
// loads this plugin, so it must equal the published package name. A mismatch
Expand Down Expand Up @@ -193,6 +236,8 @@ const activityPanelCss = await readFile(new URL('../src/client/ActivityPanel.mod
const activityPanelSource = await readFile(new URL('../src/client/ActivityPanel.tsx', import.meta.url), 'utf8')
const stagingPlanSource = await readFile(new URL('../src/client/StagingPlanEditor.tsx', import.meta.url), 'utf8')
const clientIndexSource = await readFile(new URL('../src/client/index.tsx', import.meta.url), 'utf8')
const settingsCardSource = await readFile(new URL('../src/client/AgentTeamsSettingsCard.tsx', import.meta.url), 'utf8')
const settingsModuleSource = await readFile(new URL('../src/settings.ts', import.meta.url), 'utf8')
const agentTeamsCardCss = await readFile(new URL('../src/client/AgentTeamsCard.module.css', import.meta.url), 'utf8')
const agentTeamsCardSource = await readFile(new URL('../src/client/AgentTeamsCard.tsx', import.meta.url), 'utf8')
const artworkSource = await readFile(new URL('../src/client/artwork.ts', import.meta.url), 'utf8')
Expand All @@ -215,13 +260,46 @@ check(
&& clientIndexSource.includes("'uiConversation', 'slots', 'sessions', 'locale', 'modelDirectories'")
&& clientIndexSource.includes('ctx.uiConversation.events.register(agentTeamsCardDefinition)')
&& clientIndexSource.includes('ctx.locale.register(AGENT_TEAMS_LOCALE_NAMESPACE, { zh, en })')
&& clientIndexSource.match(/locale:\s*AGENT_TEAMS_LOCALE_NAMESPACE/gu)?.length === 2,
// Activity overlay, conversation card, and the Parallel Emission settings card.
&& clientIndexSource.match(/locale:\s*AGENT_TEAMS_LOCALE_NAMESPACE/gu)?.length === 3,
)
check(
'slash command transcript hides the duplicate pre-message result row',
clientIndexSource.includes('HiddenAgentTeamsCommand')
&& /name:\s*'conversation\.chat\.commandview',\s*key:\s*'agent-teams'/u.test(clientIndexSource),
)
check(
'host registers the agent-teams settings scope before the usage snapshot and threads the live switch through',
hostSource.includes("ctx.inject(['settings']")
&& hostSource.includes('settings.register(AGENT_TEAMS_SETTINGS_NAMESPACE, AgentTeamsSettingsSchema)')
&& hostSource.includes("ctx.get('settings')")
&& /readParallelToolCalls = \(\): boolean => false/u.test(hostSource)
&& /usageSectionText\(TEAM_TOOL_NAMES\.join\(', '\), formatProfilesForPrompt\(config\.profiles\), readParallelToolCalls\(\)\)/u.test(hostSource),
)
check(
'settings module declares exactly the Parallel Emission checkbox',
settingsModuleSource.includes("AGENT_TEAMS_SETTINGS_NAMESPACE = 'agent-teams'")
&& settingsModuleSource.includes('parallelToolCalls: z.boolean().default(false)')
&& !settingsModuleSource.includes('maxParallelToolCalls'),
)
check(
'plugins-tab card is keyed by the agent-teams namespace through type-only contracts',
/import type \{\} from '@deepseek-ai\/dsh-client-ui-settings\/client'/u.test(clientIndexSource)
&& /import type \{\} from '@deepseek-ai\/dsh-client-ui-settings-plugins\/client'/u.test(clientIndexSource)
&& clientIndexSource.includes("ctx.inject(['settingsScope']")
&& clientIndexSource.includes("name: 'settings.plugin.item'")
&& clientIndexSource.includes('key: AGENT_TEAMS_SETTINGS_KEY')
&& settingsCardSource.includes("AGENT_TEAMS_SETTINGS_KEY = 'agent-teams'")
&& settingsCardSource.includes("scope.set('parallelToolCalls'")
&& !clientIndexSource.includes('maxParallelToolCalls')
&& !settingsCardSource.includes('maxParallelToolCalls'),
)
check(
'the checkbox never touches the Agent Loop execution pool knob',
!toolsSource.includes('maxParallelToolCalls')
&& !hostSource.includes('maxParallelToolCalls')
&& !localesSource.includes('maxParallelToolCalls'),
)
check(
'stop-team control lives in the team panel and requires confirmation',
!clientIndexSource.includes("conversation.input.dock")
Expand Down
47 changes: 47 additions & 0 deletions src/client/AgentTeamsSettingsCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.root {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
min-width: 0;
padding: 10px 12px;
box-sizing: border-box;
border: 1px solid var(--dsw-alias-line-normal);
border-radius: 10px;
background: var(--dsw-alias-bg-module-platform);
}

.row {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}

.row input[type='checkbox'] {
flex: none;
width: 15px;
height: 15px;
margin: 0;
accent-color: var(--dsw-alias-state-business-primary);
}

.row input[type='checkbox']:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.label {
min-width: 0;
font-size: 13px;
font-weight: 500;
color: var(--dsw-alias-label-primary);
}

.hint {
min-width: 0;
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--dsw-alias-label-tertiary);
}
67 changes: 67 additions & 0 deletions src/client/AgentTeamsSettingsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* AgentTeams settings card: the single Parallel Emission checkbox inside the
* Plugins configuration tab. The card is registered under the
* `settings.plugin.item` keyed slot with key `agent-teams`, so the tab pairs
* it with the Host-side namespace of the same name without learning what the
* namespace means (type-only slot contract).
*
* Type-only cross-plugin imports: the scope arrives through the cordis
* `settingsScope` service and writes go through it too, so the client bundle
* purity gate never sees a cross-plugin value import.
* @module dsh-agent-teams/client/settings-card
*/

import { useSyncExternalStore } from 'react'
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import css from './AgentTeamsSettingsCard.module.css'

/**
* Settings namespace this card edits. Must equal the Host-side registration
* (`AGENT_TEAMS_SETTINGS_NAMESPACE` in src/settings.ts); kept as a literal so
* the browser bundle never value-imports the host settings module.
*/
export const AGENT_TEAMS_SETTINGS_KEY = 'agent-teams'

/** The bound `agent-teams` namespace scope handed to the card. */
export type AgentTeamsSettingsScope = SettingsScope<{ parallelToolCalls: boolean }>

/** Injected share: the scope bound on this plugin's client fiber. */
export interface AgentTeamsSettingsCardInjected {
readonly scope: AgentTeamsSettingsScope
}

/** Complete keyed settings-card props. */
export type AgentTeamsSettingsCardProps =
PropsRuntime<'settings.plugin.item', typeof AGENT_TEAMS_SETTINGS_KEY>
& PropsLocale<'agentTeams'>
& AgentTeamsSettingsCardInjected

/**
* Render the Parallel Emission checkbox. The checkbox is disabled unless the
* namespace is served by the Host and the settings document accepts writes
* (non-loopback pages run in memory mode); the hint always explains what the
* switch does and what it deliberately does not touch.
*/
export function AgentTeamsSettingsCard({ t, scope }: AgentTeamsSettingsCardProps) {
const snapshot = useSyncExternalStore(scope.subscribe, scope.getSnapshot)
const ready = snapshot.status === 'ready'
const writable = ready && snapshot.writable
const checked = snapshot.value?.parallelToolCalls === true
return (
<div className={css.root}>
<label className={css.row}>
<input
type="checkbox"
checked={checked}
disabled={!writable}
data-agent-teams-parallel={checked ? 'on' : 'off'}
onChange={(event) => { void scope.set('parallelToolCalls', event.currentTarget.checked) }}
/>
<span className={css.label}>{t('settings.parallel.label')}</span>
</label>
<p className={css.hint}>{t('settings.parallel.hint')}</p>
{ready && !writable ? <p className={css.hint}>{t('settings.parallel.readOnly')}</p> : null}
</div>
)
}
Loading
Loading