mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(team): add experimental Agent Teams CLI profile
This commit is contained in:
@@ -10,7 +10,7 @@ The subagent seam supplies fresh/fork providers, durable child Sessions, FIFO fo
|
|||||||
|
|
||||||
All same-process Agents also share one checkout. Filesystem edit tools can reject an observed stale version, but Bash, formatters, generators, and external writers bypass that fence. Treating a teammate name or task owner as a file lock would hide rather than solve this concurrency boundary.
|
All same-process Agents also share one checkout. Filesystem edit tools can reject an observed stale version, but Bash, formatters, generators, and external writers bypass that fence. Treating a teammate name or task owner as a file lock would hide rather than solve this concurrency boundary.
|
||||||
|
|
||||||
The model-visible Team tools remain opt-in so the default tool catalog and simple-task behavior do not change. An explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
Agent Teams needs an explicit source-checkout composition before its public contracts are stable enough for released CLI or Web bundles. The default tool catalog and simple-task behavior must remain unchanged, while an explicitly requested Team must survive child Activation settlement and mailbox delivery races long enough for the Lead to aggregate the result before process teardown.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Worktree isolation is not a harness runtime behavior. A deployment or prompt may
|
|||||||
|
|
||||||
**Create isolated worktrees automatically.** Rejected because worktree creation, branch naming, merge policy, ignored files, build artifacts, and cleanup are deployment choices. It also changes the same-world behavior existing subagents and sandboxes expose.
|
**Create isolated worktrees automatically.** Rejected because worktree creation, branch naming, merge policy, ignored files, build artifacts, and cleanup are deployment choices. It also changes the same-world behavior existing subagents and sandboxes expose.
|
||||||
|
|
||||||
**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. Explicit composition keeps model-visible ownership unambiguous without changing shipped requests.
|
**Enable Teams in the default catalog.** Rejected because scoped Team controls would shadow same-named legacy globals and unsolicited delegation would add latency and token cost to simple tasks. A private profile bundle inserts Team and disables the legacy controls without adding Team packages to shipped dependency graphs.
|
||||||
|
|
||||||
**Use an in-memory board and mailbox.** Rejected because child settlement, HMR, and process interruption would lose accepted coordination state and make retries ambiguous.
|
**Use an in-memory board and mailbox.** Rejected because child settlement, HMR, and process interruption would lose accepted coordination state and make retries ambiguous.
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Worktree isolation is not a harness runtime behavior. A deployment or prompt may
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Package tests cover identity, name and authority checks, provider selection, reserved-id persistence collisions, child-before-Lead flush ordering, durable provisioning failure and pending-inbox JSONL/SQLite reconciliation, concurrent target-local ordering, pending/history de-duplication, mailbox limits, post-flush notification, bounded disposal with in-flight creation and dispatch cancellation, failed-member cleanup, task CAS and DAG validation, write-scope warnings, wait cancellation/timeout, inbox-preserving interruption, ordinary-fork isolation, legacy-control shadowing, compact declared-schema result rendering, and scoped registration HMR at per-file 100% coverage. A keyless headless Loader snapshot assembles the real Team plugins and records teammate creation, peer mail, dependent tasks, waiting, and Lead aggregation.
|
Package tests cover identity, name and authority checks, provider selection, reserved-id persistence collisions, child-before-Lead flush ordering, durable provisioning failure and pending-inbox JSONL/SQLite reconciliation, concurrent target-local ordering, pending/history de-duplication, mailbox limits, post-flush notification, bounded disposal with in-flight creation and dispatch cancellation, failed-member cleanup, task CAS and DAG validation, write-scope warnings, wait cancellation/timeout, inbox-preserving interruption, ordinary-fork isolation, legacy-control shadowing, compact declared-schema result rendering, and scoped registration HMR at per-file 100% coverage. A keyless headless Loader snapshot assembles the real Team plugins and exercises two teammates, dependent tasks, peer delivery, waiting, completion, and Lead aggregation. A CLI product test loads the private Agent Teams profile bundle, starts `dsh run` through the real Loader and direct core front door, and verifies normal process exit with persisted Team and child logs.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ subagent seam 已提供 fresh/fork provider、持久 child Session、FIFO foll
|
|||||||
|
|
||||||
同进程 Agent 还共享一个 checkout。文件系统 edit 工具可以拒绝已观察到的陈旧版本,但 Bash、formatter、generator 与外部 writer 会绕过该屏障。把 teammate name 或 task owner 当作文件锁只会掩盖而不是解决该并发边界。
|
同进程 Agent 还共享一个 checkout。文件系统 edit 工具可以拒绝已观察到的陈旧版本,但 Bash、formatter、generator 与外部 writer 会绕过该屏障。把 teammate name 或 task owner 当作文件锁只会掩盖而不是解决该并发边界。
|
||||||
|
|
||||||
面向模型的 Team 工具保持显式启用,使默认工具目录与简单任务行为不变。显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
在公开约定稳定到足以进入已发布 CLI 或 Web bundle 前,Agent Teams 需要显式的源码 checkout 组合。默认工具目录与简单任务行为必须保持不变;而显式请求的 Team 必须能跨越 child Activation settlement 与 mailbox 投递竞争,使 Lead 在进程 teardown 前汇总结果。
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以
|
|||||||
|
|
||||||
**自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。
|
**自动创建隔离 worktree。** 拒绝,因为 worktree 创建、branch 命名、merge 策略、ignored file、构建产物与 cleanup 都是 deployment 选择;它也会改变既有 subagent 与 sandbox 暴露的 same-world 行为。
|
||||||
|
|
||||||
**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。显式组合可以保持面向模型的归属明确,同时不改变默认 request。
|
**在默认工具目录中启用 Team。** 拒绝,因为 scoped Team control 会覆盖同名旧全局工具,主动 delegation 也会给简单任务增加延迟和 token 成本。私有 profile bundle 会插入 Team 并禁用旧 control,同时不向已发布依赖图添加 Team 包。
|
||||||
|
|
||||||
**使用内存 task board 与 mailbox。** 拒绝,因为 child settlement、HMR 与进程中断会丢失已接受协调状态,并让重试变得含糊。
|
**使用内存 task board 与 mailbox。** 拒绝,因为 child settlement、HMR 与进程中断会丢失已接受协调状态,并让重试变得含糊。
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ Worktree isolation 不是 harness runtime 行为。deployment 或 prompt 可以
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、provider 选择、预留 id 持久化冲突、child-before-Lead flush 顺序、持久 provisioning 失败与 pending-inbox JSONL/SQLite 对账、target-local 并发顺序、pending/history 去重、mailbox 限额、flush 后 notification、取消在途创建与 dispatch 的有界 dispose、failed member cleanup、task CAS 与 DAG 校验、write-scope warning、wait cancel/timeout、保留 inbox 的 interrupt、普通 fork 隔离、旧 control shadowing、声明 schema 的紧凑结果渲染与 scoped registration HMR。一条 keyless headless Loader 快照会组合真实 Team 插件,并记录 teammate 创建、peer mail、依赖任务、等待与 Lead 汇总。
|
Package test 以逐文件 100% coverage 覆盖身份、名字与权限检查、provider 选择、预留 id 持久化冲突、child-before-Lead flush 顺序、持久 provisioning 失败与 pending-inbox JSONL/SQLite 对账、target-local 并发顺序、pending/history 去重、mailbox 限额、flush 后 notification、取消在途创建与 dispatch 的有界 dispose、failed member cleanup、task CAS 与 DAG 校验、write-scope warning、wait cancel/timeout、保留 inbox 的 interrupt、普通 fork 隔离、旧 control shadowing、声明 schema 的紧凑结果渲染与 scoped registration HMR。一条 keyless headless Loader 快照会组合真实 Team 插件,并运行两个 teammate、依赖任务、peer 投递、等待、完成与 Lead 汇总。CLI 产品测试会加载私有 Agent Teams profile bundle,通过真实 Loader 与直接 core front door 启动 `dsh run`,并验证带持久 Team 与 child 日志的正常退出。
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||||
|
import { execa } from 'execa'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
|
||||||
|
|
||||||
|
const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
|
||||||
|
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
|
||||||
|
const fixturePlugin = pathToFileURL(fileURLToPath(new URL('./fixtures/team-llm.mjs', import.meta.url))).href
|
||||||
|
|
||||||
|
function records(content: string): Record<string, unknown>[] {
|
||||||
|
return content.split('\n').filter(Boolean).map(line => JSON.parse(line) as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dsh run with Agent Teams enabled', () => {
|
||||||
|
it('runs two teammates, durable peer mail, dependent tasks, waiting, and final aggregation', async () => {
|
||||||
|
const cwd = await mkdtemp(join(tmpdir(), 'dsh-agent-team-headless-'))
|
||||||
|
try {
|
||||||
|
const home = join(cwd, '.dsh')
|
||||||
|
const sessions = join(home, 'sessions')
|
||||||
|
const profileDir = join(home, 'profiles', 'headless')
|
||||||
|
await mkdir(profileDir, { recursive: true })
|
||||||
|
await writeFile(join(profileDir, 'package.json'), JSON.stringify({
|
||||||
|
name: 'dsh-profile-headless',
|
||||||
|
private: true,
|
||||||
|
dependencies: {
|
||||||
|
'@deepseek-ai/dsh-agent-team-profile': 'workspace:^',
|
||||||
|
},
|
||||||
|
dsh: {
|
||||||
|
profile: {
|
||||||
|
bundles: [
|
||||||
|
'@deepseek-ai/dsh-base',
|
||||||
|
'@deepseek-ai/dsh-headless',
|
||||||
|
'@deepseek-ai/dsh-agent-team-profile',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, undefined, 2) + '\n')
|
||||||
|
await writeFile(join(profileDir, 'cordis.patch.yml'), [
|
||||||
|
'- id: llm-deepseek',
|
||||||
|
' disabled: true',
|
||||||
|
'- id: session-persistence-jsonl',
|
||||||
|
' config:',
|
||||||
|
` root: '${sessions}'`,
|
||||||
|
' compression: none',
|
||||||
|
'- insert:',
|
||||||
|
' - id: team-fixture-llm',
|
||||||
|
` name: '${fixturePlugin}'`,
|
||||||
|
'',
|
||||||
|
].join('\n'))
|
||||||
|
const launch = resolveExampleLaunch({
|
||||||
|
srcBin: dshBinScript,
|
||||||
|
configArgs: ['--profile', 'headless', '请明确使用 Agent Teams,把调研和实现拆给两个 teammate,等待完成后汇总。'],
|
||||||
|
tsconfigPath,
|
||||||
|
env: {
|
||||||
|
DSH_HOME: home,
|
||||||
|
DSH_AGENTS_HOME: join(cwd, '.agents'),
|
||||||
|
DSH_TELEMETRY_DISABLED: '1',
|
||||||
|
DEEPSEEK_API_KEY: '',
|
||||||
|
NODE_OPTIONS: [
|
||||||
|
process.env.NODE_OPTIONS,
|
||||||
|
'--disable-warning=ExperimentalWarning',
|
||||||
|
'--disable-warning=MODULE_TYPELESS_PACKAGE_JSON',
|
||||||
|
].filter(Boolean).join(' '),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const result = await execa(launch.command, launch.args, {
|
||||||
|
cwd,
|
||||||
|
env: launch.env,
|
||||||
|
input: '',
|
||||||
|
timeout: 90_000,
|
||||||
|
killSignal: 'SIGKILL',
|
||||||
|
reject: false,
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
result.exitCode,
|
||||||
|
`dsh headless profile exited unexpectedly.\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
||||||
|
).toBe(0)
|
||||||
|
expect(result.stderr).toBe('')
|
||||||
|
expect(result.stdout).toContain('TEAM_WORKFLOW_OK')
|
||||||
|
|
||||||
|
const files = (await readdir(sessions, { recursive: true }))
|
||||||
|
.filter(file => file.endsWith('.jsonl'))
|
||||||
|
expect(files).toHaveLength(3)
|
||||||
|
const logs = await Promise.all(files.map(file => readFile(join(sessions, file), 'utf8')))
|
||||||
|
const parsed = logs.map(records)
|
||||||
|
const root = parsed.find((log) => {
|
||||||
|
const header = log[0]
|
||||||
|
return header?.type === 'session' && typeof header.parentSession !== 'string'
|
||||||
|
})
|
||||||
|
expect(root).toBeDefined()
|
||||||
|
const eventTypes = root!.map(record => record.type)
|
||||||
|
expect(eventTypes.filter(type => type === 'team/member')).toHaveLength(4)
|
||||||
|
expect(eventTypes).toContain('team/message/queued')
|
||||||
|
expect(eventTypes).toContain('team/message/delivered')
|
||||||
|
const taskEvents = root!.filter(record => record.type === 'team/task')
|
||||||
|
expect(taskEvents.filter((record) => {
|
||||||
|
const data = record.data as { task?: { status?: string } } | undefined
|
||||||
|
return data?.task?.status === 'completed'
|
||||||
|
})).toHaveLength(2)
|
||||||
|
const toolNames = root!.filter(record => record.type === 'tool/call')
|
||||||
|
.map(record => (record.data as { name?: string } | undefined)?.name)
|
||||||
|
expect(toolNames).toContain('wait_agent')
|
||||||
|
expect(toolNames).toContain('team_task_list')
|
||||||
|
expect(toolNames).toContain('list_agents')
|
||||||
|
} finally {
|
||||||
|
await rm(cwd, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
}, 105_000)
|
||||||
|
})
|
||||||
Vendored
+194
@@ -0,0 +1,194 @@
|
|||||||
|
/** Deterministic keyless Agent Teams adapter for the real `dsh run` product test. */
|
||||||
|
|
||||||
|
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||||
|
|
||||||
|
let nextCall = 0
|
||||||
|
|
||||||
|
function calls(messages) {
|
||||||
|
return messages.flatMap(message => message.role === 'assistant'
|
||||||
|
? message.content.filter(block => block.type === 'tool-call').map(block => block.name)
|
||||||
|
: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestAssistantCalls(messages) {
|
||||||
|
const assistant = messages.findLast(message => message.role === 'assistant')
|
||||||
|
return assistant?.content.filter(block => block.type === 'tool-call').map(block => block.name) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTaskAction(messages, action) {
|
||||||
|
return messages.some(message => message.role === 'assistant'
|
||||||
|
&& message.content.some((block) => {
|
||||||
|
if (block.type !== 'tool-call' || block.name !== 'team_task_update') return false
|
||||||
|
try {
|
||||||
|
return JSON.parse(block.arguments).action === action
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function latestToolText(messages) {
|
||||||
|
const message = messages.findLast(candidate => candidate.content.some(block => block.type === 'tool-result'))
|
||||||
|
if (message === undefined) return ''
|
||||||
|
return message.content.flatMap(block => block.type === 'tool-result'
|
||||||
|
? block.content.filter(item => item.type === 'text').map(item => item.text)
|
||||||
|
: []).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toolChunks(specs) {
|
||||||
|
const chunks = []
|
||||||
|
for (const [index, spec] of specs.entries()) {
|
||||||
|
const id = CallId(`team-fixture-${++nextCall}`)
|
||||||
|
const args = JSON.stringify(spec.args)
|
||||||
|
chunks.push(
|
||||||
|
{ type: 'block-start', index, blockType: 'tool-call' },
|
||||||
|
{ type: 'tool-call-delta', index, id, name: spec.name, argumentsDelta: args },
|
||||||
|
{ type: 'block-end', index, block: { type: 'tool-call', id, name: spec.name, arguments: args } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
chunks.push(
|
||||||
|
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } },
|
||||||
|
{ type: 'finish', reason: { kind: 'tool-calls' } },
|
||||||
|
)
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
|
|
||||||
|
function textChunks(text) {
|
||||||
|
return [
|
||||||
|
{ type: 'block-start', index: 0, blockType: 'text' },
|
||||||
|
{ type: 'text-delta', index: 0, text },
|
||||||
|
{ type: 'block-end', index: 0, block: { type: 'text', text } },
|
||||||
|
{ type: 'usage', usage: { inputTokens: 10, outputTokens: 3 } },
|
||||||
|
{ type: 'finish', reason: { kind: 'stop' } },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function researcher(messages) {
|
||||||
|
const names = calls(messages)
|
||||||
|
if (!names.includes('team_task_create')) {
|
||||||
|
return toolChunks([{ name: 'team_task_create', args: {
|
||||||
|
subject: 'Research', description: 'Collect the deterministic finding.', write_scopes: ['research'],
|
||||||
|
} }])
|
||||||
|
}
|
||||||
|
if (!names.includes('team_task_update')) {
|
||||||
|
return toolChunks([{ name: 'team_task_update', args: {
|
||||||
|
task_id: 'task-1', expected_revision: 1, action: 'claim',
|
||||||
|
} }])
|
||||||
|
}
|
||||||
|
if (!names.includes('send_message')) {
|
||||||
|
return toolChunks([
|
||||||
|
{ name: 'team_task_update', args: { task_id: 'task-1', expected_revision: 2, action: 'complete' } },
|
||||||
|
{ name: 'send_message', args: { target: 'implementer', message: 'Research complete: use the deterministic finding.' } },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return textChunks('Research teammate complete.')
|
||||||
|
}
|
||||||
|
|
||||||
|
function implementer(messages) {
|
||||||
|
const names = calls(messages)
|
||||||
|
const last = latestAssistantCalls(messages)
|
||||||
|
const text = latestToolText(messages)
|
||||||
|
if (!names.includes('team_task_create')) {
|
||||||
|
if (last.includes('team_task_get') && text.includes('"subject":"Research"')) {
|
||||||
|
return toolChunks([{ name: 'team_task_create', args: {
|
||||||
|
subject: 'Implementation',
|
||||||
|
description: 'Apply the deterministic finding.',
|
||||||
|
blocked_by: ['task-1'],
|
||||||
|
write_scopes: ['implementation'],
|
||||||
|
} }])
|
||||||
|
}
|
||||||
|
if (last.includes('wait_agent')) {
|
||||||
|
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||||
|
}
|
||||||
|
return toolChunks([{ name: 'wait_agent', args: { timeout_ms: 10000 } }])
|
||||||
|
}
|
||||||
|
if (!hasTaskAction(messages, 'claim')) {
|
||||||
|
if (last.includes('team_task_get') && text.includes('"status":"completed"')) {
|
||||||
|
return toolChunks([{ name: 'team_task_update', args: {
|
||||||
|
task_id: 'task-2', expected_revision: 1, action: 'claim',
|
||||||
|
} }])
|
||||||
|
}
|
||||||
|
if (last.includes('team_task_get')) {
|
||||||
|
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||||
|
}
|
||||||
|
if (last.includes('wait_agent')) {
|
||||||
|
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||||
|
}
|
||||||
|
return toolChunks([{ name: 'team_task_get', args: { task_id: 'task-1' } }])
|
||||||
|
}
|
||||||
|
if (!names.includes('send_message')) {
|
||||||
|
return toolChunks([
|
||||||
|
{ name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } },
|
||||||
|
{ name: 'send_message', args: { target: 'lead', message: 'Implementation complete and verified.' } },
|
||||||
|
])
|
||||||
|
}
|
||||||
|
return textChunks('Implementation teammate complete.')
|
||||||
|
}
|
||||||
|
|
||||||
|
function lead(messages) {
|
||||||
|
const names = calls(messages)
|
||||||
|
const last = latestAssistantCalls(messages)
|
||||||
|
const spawned = names.filter(name => name === 'spawn_teammate').length
|
||||||
|
if (spawned === 0) {
|
||||||
|
return toolChunks([{
|
||||||
|
name: 'spawn_teammate',
|
||||||
|
args: {
|
||||||
|
name: 'implementer',
|
||||||
|
description: 'Own deterministic implementation.',
|
||||||
|
prompt: 'IMPLEMENTER_MARK: wait for research, complete dependent task 2, report to lead.',
|
||||||
|
context: 'fresh',
|
||||||
|
},
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
if (spawned === 1) {
|
||||||
|
return toolChunks([{
|
||||||
|
name: 'spawn_teammate',
|
||||||
|
args: {
|
||||||
|
name: 'researcher',
|
||||||
|
description: 'Own deterministic research.',
|
||||||
|
prompt: 'RESEARCHER_MARK: complete research task 1, message implementer, then finish.',
|
||||||
|
context: 'fresh',
|
||||||
|
},
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
const result = latestToolText(messages)
|
||||||
|
if (last.includes('team_task_list')) {
|
||||||
|
const completed = result.match(/"status":"completed"/gu)?.length ?? 0
|
||||||
|
if (completed >= 2) return toolChunks([{ name: 'list_agents', args: {} }])
|
||||||
|
return toolChunks([{ name: 'team_task_list', args: {} }])
|
||||||
|
}
|
||||||
|
if (last.includes('list_agents')) {
|
||||||
|
const inactive = result.match(/"status":"inactive"/gu)?.length ?? 0
|
||||||
|
if (inactive >= 2) return textChunks('TEAM_WORKFLOW_OK: both teammates and dependent tasks completed.')
|
||||||
|
return toolChunks([{ name: 'list_agents', args: {} }])
|
||||||
|
}
|
||||||
|
if (last.includes('wait_agent')) return toolChunks([{ name: 'team_task_list', args: {} }])
|
||||||
|
return toolChunks([{ name: 'wait_agent', args: { timeout_ms: 10000 } }])
|
||||||
|
}
|
||||||
|
|
||||||
|
class TeamFixtureAdapter extends LlmAdapter {
|
||||||
|
async * stream(options) {
|
||||||
|
const userText = options.messages.flatMap(message => message.role === 'user'
|
||||||
|
? message.content.filter(block => block.type === 'text').map(block => block.text)
|
||||||
|
: []).join('\n')
|
||||||
|
const chunks = userText.includes('RESEARCHER_MARK')
|
||||||
|
? researcher(options.messages)
|
||||||
|
: userText.includes('IMPLEMENTER_MARK')
|
||||||
|
? implementer(options.messages)
|
||||||
|
: lead(options.messages)
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
options.signal?.throwIfAborted()
|
||||||
|
yield chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cordis plugin name. */
|
||||||
|
export const name = 'team-fixture-llm'
|
||||||
|
/** LLM registry dependency. */
|
||||||
|
export const inject = ['llm']
|
||||||
|
|
||||||
|
/** Register the keyless adapter on the shipped default provider route. */
|
||||||
|
export function apply(ctx) {
|
||||||
|
ctx.llm.registerAdapter(['deepseek-official'], new TeamFixtureAdapter())
|
||||||
|
}
|
||||||
@@ -3418,6 +3418,7 @@ Abstract service classes — a deployment loads a concrete implementation packag
|
|||||||
Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
Imported as libraries by other packages; a `cordis.yml` cannot load them.
|
||||||
|
|
||||||
- `@deepseek-ai/dsh-agent-loop-testkit` ([`packages/test-support/agent-loop-testkit/src/index.ts`](../packages/test-support/agent-loop-testkit/src/index.ts))
|
- `@deepseek-ai/dsh-agent-loop-testkit` ([`packages/test-support/agent-loop-testkit/src/index.ts`](../packages/test-support/agent-loop-testkit/src/index.ts))
|
||||||
|
- `@deepseek-ai/dsh-agent-team-profile` ([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-anonymous-user-id` ([`packages/identity/anonymous-user-id/src/index.ts`](../packages/identity/anonymous-user-id/src/index.ts))
|
- `@deepseek-ai/dsh-anonymous-user-id` ([`packages/identity/anonymous-user-id/src/index.ts`](../packages/identity/anonymous-user-id/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-app-boot` ([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
|
- `@deepseek-ai/dsh-app-boot` ([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-atomic-write` ([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts))
|
- `@deepseek-ai/dsh-atomic-write` ([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts))
|
||||||
|
|||||||
@@ -3419,6 +3419,7 @@ export interface Config {
|
|||||||
由其他包作为库导入;`cordis.yml` 无法加载它们。
|
由其他包作为库导入;`cordis.yml` 无法加载它们。
|
||||||
|
|
||||||
- `@deepseek-ai/dsh-agent-loop-testkit`([`packages/test-support/agent-loop-testkit/src/index.ts`](../packages/test-support/agent-loop-testkit/src/index.ts))
|
- `@deepseek-ai/dsh-agent-loop-testkit`([`packages/test-support/agent-loop-testkit/src/index.ts`](../packages/test-support/agent-loop-testkit/src/index.ts))
|
||||||
|
- `@deepseek-ai/dsh-agent-team-profile`([`packages/experimental/agent-team-profile/src/index.ts`](../packages/experimental/agent-team-profile/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-anonymous-user-id`([`packages/identity/anonymous-user-id/src/index.ts`](../packages/identity/anonymous-user-id/src/index.ts))
|
- `@deepseek-ai/dsh-anonymous-user-id`([`packages/identity/anonymous-user-id/src/index.ts`](../packages/identity/anonymous-user-id/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-app-boot`([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
|
- `@deepseek-ai/dsh-app-boot`([`packages/boot/app-boot/src/index.ts`](../packages/boot/app-boot/src/index.ts))
|
||||||
- `@deepseek-ai/dsh-atomic-write`([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts))
|
- `@deepseek-ai/dsh-atomic-write`([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts))
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||||
|
# 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-profile/README.md
|
||||||
|
README.md: 1de882f08ebd0ffe0ad6efbc3729db4e75b488e1
|
||||||
|
README.zh.md: 75bc68b59dd1564e89ef0209a2f5374e8a8541ed
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# @deepseek-ai/dsh-agent-team-profile
|
||||||
|
|
||||||
|
English | [中文](README.zh.md)
|
||||||
|
|
||||||
|
Private profile bundle that enables [Agent Teams](../team/README.md) over `@deepseek-ai/dsh-base`. Its patch inserts the Team domain and scoped tools, disables the overlapping global continuable-child controls, and keeps the ordinary fresh and fork delegation tools as one-shot operations. Install this package explicitly into a source-checkout profile; it is excluded from official releases.
|
||||||
|
|
||||||
|
## Profile installation
|
||||||
|
|
||||||
|
From this repository checkout, add the package to an initialized profile:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dsh plugin --profile headless add ./packages/experimental/agent-team-profile
|
||||||
|
pnpm dsh --profile headless "Use Agent Teams to split this task between two teammates, wait, and summarize."
|
||||||
|
```
|
||||||
|
|
||||||
|
The profile must already contain `@deepseek-ai/dsh-base`, whose Subagent services and provider rows this layer consumes. Removing the package with `dsh plugin --profile <name> remove @deepseek-ai/dsh-agent-team-profile` removes the bundle from the profile's ordered layer list.
|
||||||
|
|
||||||
|
## Model Experience
|
||||||
|
|
||||||
|
### Team policy and tools
|
||||||
|
|
||||||
|
#### What the model sees
|
||||||
|
|
||||||
|
The Team policy and schemas belong to [`@deepseek-ai/dsh-tool-team`](../tool-team/README.md). This bundle changes composition only: Team-scoped `list_agents` and `send_message` replace the disabled global continuable-child controls, while `subagent` and `subagent_fork` remain available as one-shot delegation tools.
|
||||||
|
|
||||||
|
#### Token effect
|
||||||
|
|
||||||
|
The bundle adds the Team policy and tool schemas described by `dsh-tool-team`; it adds no prompt text of its own.
|
||||||
|
|
||||||
|
#### KV Cache effect
|
||||||
|
|
||||||
|
The bundle's composition is prefix-stable while its patch, Team identity, and configured tool schemas remain unchanged.
|
||||||
|
|
||||||
|
## Known Limitations and Deferred Work
|
||||||
|
|
||||||
|
- **Source-checkout only** — this private package is not present in official npm, CLI, Web, or Python release payloads.
|
||||||
|
- **Shared checkout** — every teammate observes the same working directory; this bundle adds no worktree isolation or filesystem locking.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# @deepseek-ai/dsh-agent-team-profile
|
||||||
|
|
||||||
|
[English](README.md) | 中文
|
||||||
|
|
||||||
|
在 `@deepseek-ai/dsh-base` 之上启用 [Agent Teams](../team/README.md) 的私有 profile bundle。它的 patch 会插入 Team domain 与 scoped 工具、禁用名称重叠的全局 continuable-child control,并保留普通的一次性 fresh/fork delegation 工具。必须从源码 checkout 将本包显式安装到 profile;正式发布会排除本包。
|
||||||
|
|
||||||
|
## Profile 安装
|
||||||
|
|
||||||
|
在本仓库 checkout 中,将本包添加到已初始化的 profile:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm dsh plugin --profile headless add ./packages/experimental/agent-team-profile
|
||||||
|
pnpm dsh --profile headless "Use Agent Teams to split this task between two teammates, wait, and summarize."
|
||||||
|
```
|
||||||
|
|
||||||
|
profile 必须已经包含 `@deepseek-ai/dsh-base`,本层会使用其中的 Subagent service 与 provider 配置行。执行 `dsh plugin --profile <name> remove @deepseek-ai/dsh-agent-team-profile` 移除本包时,bundle 也会从 profile 的有序层列表中移除。
|
||||||
|
|
||||||
|
## Model Experience
|
||||||
|
|
||||||
|
### Team 策略与工具
|
||||||
|
|
||||||
|
#### 模型会看到什么
|
||||||
|
|
||||||
|
Team 策略与 schema 由 [`@deepseek-ai/dsh-tool-team`](../tool-team/README.md) 所有。本 bundle 只改变 composition:Team-scoped `list_agents` 与 `send_message` 会替代已禁用的全局 continuable-child control,而 `subagent` 与 `subagent_fork` 仍作为一次性 delegation 工具可用。
|
||||||
|
|
||||||
|
#### Token 影响
|
||||||
|
|
||||||
|
本 bundle 会加入 `dsh-tool-team` 描述的 Team 策略与工具 schema;它自身不增加提示词文本。
|
||||||
|
|
||||||
|
#### KV Cache 影响
|
||||||
|
|
||||||
|
只要 bundle patch、Team identity 与配置的工具 schema 不变,本 bundle 的 composition 就保持前缀稳定。
|
||||||
|
|
||||||
|
## Known Limitations and Deferred Work
|
||||||
|
|
||||||
|
- **仅限源码 checkout**:正式 npm、CLI、Web 与 Python 发布产物都不包含这个私有包。
|
||||||
|
- **共享 checkout**:所有 teammate 都观察同一个工作目录;本 bundle 不提供 worktree 隔离或文件系统锁。
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Private Agent Teams profile layer. Apply after dsh-base so these replacements
|
||||||
|
# remove the global continuable-child controls before the scoped Team tools
|
||||||
|
# register the overlapping list_agents and send_message names.
|
||||||
|
|
||||||
|
- id: tool-subagent-control
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- id: tool-subagent-list-agents
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- id: tool-subagent-report
|
||||||
|
disabled: true
|
||||||
|
|
||||||
|
- id: tool-subagent
|
||||||
|
config:
|
||||||
|
provider: spawn
|
||||||
|
toolName: subagent
|
||||||
|
backgroundMode: one-shot
|
||||||
|
|
||||||
|
- id: tool-subagent-fork
|
||||||
|
config:
|
||||||
|
provider: fork
|
||||||
|
toolName: subagent_fork
|
||||||
|
backgroundMode: one-shot
|
||||||
|
|
||||||
|
- insert:
|
||||||
|
- id: team
|
||||||
|
name: '@deepseek-ai/dsh-team'
|
||||||
|
config:
|
||||||
|
maxMembers: 8
|
||||||
|
maxTasks: 256
|
||||||
|
maxPendingMessagesPerMember: 64
|
||||||
|
maxMessageBytes: 65536
|
||||||
|
disposalTimeoutMs: 5000
|
||||||
|
|
||||||
|
- id: tool-team
|
||||||
|
name: '@deepseek-ai/dsh-tool-team'
|
||||||
|
config:
|
||||||
|
freshProvider: spawn
|
||||||
|
forkProvider: fork
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "@deepseek-ai/dsh-agent-team-profile",
|
||||||
|
"description": "Private profile bundle enabling Agent Teams over dsh-base",
|
||||||
|
"version": "0.1.0-rc.7",
|
||||||
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||||
|
"directory": "packages/experimental/agent-team-profile"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/types/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./lib/types/index.d.ts",
|
||||||
|
"default": "./lib/index.js"
|
||||||
|
},
|
||||||
|
"./invariant": {
|
||||||
|
"types": "./lib/types/invariant.d.ts",
|
||||||
|
"default": "./lib/invariant.js"
|
||||||
|
},
|
||||||
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
||||||
|
"./src/*": "./src/*",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/index.js",
|
||||||
|
"lib/invariant.js",
|
||||||
|
"cordis.patch.yml",
|
||||||
|
"lib/types/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dsh": {
|
||||||
|
"bundle": {
|
||||||
|
"patch": "./cordis.patch.yml"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@deepseek-ai/dsh-team": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tool-team": "workspace:^"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
|
"@deepseek-ai/cordis": "workspace:^"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@deepseek-ai/cordis-plugin-include": "workspace:^",
|
||||||
|
"@deepseek-ai/cordis-plugin-loader": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||||
|
"@deepseek-ai/cordis": "workspace:^",
|
||||||
|
"js-yaml": "^4.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @deepseek-ai/dsh-agent-team-profile — private Agent Teams profile bundle.
|
||||||
|
* The package's runtime content is its `dsh.bundle.patch` document; this
|
||||||
|
* module exports no runtime API.
|
||||||
|
* @module @deepseek-ai/dsh-agent-team-profile
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Package-owned invariant companion for `@deepseek-ai/dsh-agent-team-profile`.
|
||||||
|
* @module @deepseek-ai/dsh-agent-team-profile/invariant
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
|
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||||
|
|
||||||
|
const PACKAGE_NAME = '@deepseek-ai/dsh-agent-team-profile'
|
||||||
|
|
||||||
|
/** Cordis companion plugin name. */
|
||||||
|
export const name = 'agent-team-profile-invariant'
|
||||||
|
/** Service required before the companion can register. */
|
||||||
|
export const inject = ['invariants']
|
||||||
|
|
||||||
|
// No runtime invariant: the package carries only a static profile patch. The
|
||||||
|
// Team domain and tool packages own the mutable relationships it activates.
|
||||||
|
const install: InvariantInstaller = () => {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register this package's invariant companion.
|
||||||
|
* @param ctx - Cordis context carrying the invariant service.
|
||||||
|
* @returns the installed registration's disposer after setup succeeds.
|
||||||
|
*/
|
||||||
|
export const apply = (ctx: Context): Promise<() => void> =>
|
||||||
|
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/** The experimental bundle must carry one parseable, explicit Team layer. */
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import * as yaml from 'js-yaml'
|
||||||
|
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||||
|
|
||||||
|
describe('Agent Teams profile bundle', () => {
|
||||||
|
it('declares a private parseable layer with Team-owned controls', () => {
|
||||||
|
const root = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as {
|
||||||
|
private?: boolean
|
||||||
|
publishConfig?: unknown
|
||||||
|
dependencies?: Record<string, string>
|
||||||
|
dsh?: { bundle?: { patch?: string } }
|
||||||
|
}
|
||||||
|
expect(manifest.private).toBe(true)
|
||||||
|
expect(manifest.publishConfig).toBeUndefined()
|
||||||
|
expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml')
|
||||||
|
expect(manifest.dependencies).toMatchObject({
|
||||||
|
'@deepseek-ai/dsh-team': 'workspace:^',
|
||||||
|
'@deepseek-ai/dsh-tool-team': 'workspace:^',
|
||||||
|
})
|
||||||
|
|
||||||
|
const parsed = yaml.load(
|
||||||
|
readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'),
|
||||||
|
{ schema: entryListSchema },
|
||||||
|
)
|
||||||
|
expect(Array.isArray(parsed)).toBe(true)
|
||||||
|
const patches = parsed as {
|
||||||
|
id?: string
|
||||||
|
disabled?: boolean
|
||||||
|
config?: Record<string, unknown>
|
||||||
|
insert?: { id?: string; name?: string; config?: Record<string, unknown> }[]
|
||||||
|
}[]
|
||||||
|
expect(patches.find(patch => patch.id === 'tool-subagent-control')).toMatchObject({ disabled: true })
|
||||||
|
expect(patches.find(patch => patch.id === 'tool-subagent-list-agents')).toMatchObject({ disabled: true })
|
||||||
|
expect(patches.find(patch => patch.id === 'tool-subagent-report')).toMatchObject({ disabled: true })
|
||||||
|
expect(patches.find(patch => patch.id === 'tool-subagent')?.config).toMatchObject({ backgroundMode: 'one-shot' })
|
||||||
|
expect(patches.find(patch => patch.id === 'tool-subagent-fork')?.config).toMatchObject({ backgroundMode: 'one-shot' })
|
||||||
|
const inserted = patches.flatMap(patch => patch.insert ?? [])
|
||||||
|
expect(inserted.find(entry => entry.id === 'team')).toMatchObject({
|
||||||
|
name: '@deepseek-ai/dsh-team',
|
||||||
|
config: { maxMembers: 8 },
|
||||||
|
})
|
||||||
|
expect(inserted.find(entry => entry.id === 'tool-team')).toMatchObject({
|
||||||
|
name: '@deepseek-ai/dsh-tool-team',
|
||||||
|
config: { freshProvider: 'spawn', forkProvider: 'fork' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "lib/types"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../vendor/cordis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../runtime-diagnostics/invariants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../team"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../tool-team"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -838,6 +838,28 @@ describe('Team shared task DAG', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Team mailbox and waiting', () => {
|
describe('Team mailbox and waiting', () => {
|
||||||
|
it('injects a quiet message addressed to the Lead and checkpoints its receipt', async () => {
|
||||||
|
const { ctx, lead } = await setup([])
|
||||||
|
const message: TeamMessageSnapshot = {
|
||||||
|
id: TeamMessageId('quiet-lead-message'),
|
||||||
|
senderId: SessionId('team-worker'),
|
||||||
|
senderName: 'worker',
|
||||||
|
targetId: lead.id,
|
||||||
|
delivery: 'quiet',
|
||||||
|
content: content('quiet report'),
|
||||||
|
}
|
||||||
|
lead.session.append('team/message/queued', {
|
||||||
|
version: 1,
|
||||||
|
teamId: TeamId(lead.id),
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(teamInternals(ctx).mailbox.tryDispatch(lead, message, SIGNAL)).resolves.toBe(true)
|
||||||
|
expect(lead.inbox.nextStep.some(input => input.source.kind === 'team-message'
|
||||||
|
&& input.source.messageId === message.id)).toBe(true)
|
||||||
|
expect(durable(lead).pendingMessages).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
it('acknowledges waking messages persisted by a busy Lead before model claim', async () => {
|
it('acknowledges waking messages persisted by a busy Lead before model claim', async () => {
|
||||||
const { ctx, lead, teamFiber } = await setup(['hang', 'hang'], { maxPendingMessagesPerMember: 1 })
|
const { ctx, lead, teamFiber } = await setup(['hang', 'hang'], { maxPendingMessagesPerMember: 1 })
|
||||||
const started = await spawn(ctx, lead, 'lead-reporter')
|
const started = await spawn(ctx, lead, 'lead-reporter')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import {
|
import {
|
||||||
checkExperimentalDependencyIsolation,
|
checkExperimentalDependencyIsolation,
|
||||||
checkExperimentalManifest,
|
checkExperimentalManifest,
|
||||||
|
expectedDshPackageFiles,
|
||||||
type WorkspaceManifest,
|
type WorkspaceManifest,
|
||||||
} from './check-workspace-constraints.ts'
|
} from './check-workspace-constraints.ts'
|
||||||
|
|
||||||
@@ -74,3 +75,17 @@ describe('experimental workspace constraints', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('package payload constraints', () => {
|
||||||
|
it('includes a declared profile patch without a package-name allowlist', () => {
|
||||||
|
expect(expectedDshPackageFiles({
|
||||||
|
name: '@deepseek-ai/dsh-private-profile',
|
||||||
|
dsh: { bundle: { patch: './cordis.patch.yml' } },
|
||||||
|
})).toEqual([
|
||||||
|
'lib/index.js',
|
||||||
|
'lib/invariant.js',
|
||||||
|
'cordis.patch.yml',
|
||||||
|
'lib/types/**/*.d.ts',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
|
|||||||
return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
export function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||||
const declaredPatch = manifest.dsh?.bundle?.patch
|
const declaredPatch = manifest.dsh?.bundle?.patch
|
||||||
const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
|
const bundleFiles = declaredPatch === undefined ? [] : [declaredPatch.replace(/^\.\//, '')]
|
||||||
const extras = [
|
const extras = [
|
||||||
|
|||||||
Reference in New Issue
Block a user