refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -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/subagent/README.md
README.md: 134028c9993255464c08d912d305c65ed85d65c0
README.zh.md: b212ef123ba6da227f427b23bbff84264eaf80a4
README.md: a863ed3f5ef864b6eb6eb9a7a0c1ee2f40f247d6
README.zh.md: 1c9bf8ba0814a74c5774d81a34a3266daa9c375b

View File

@@ -7,9 +7,9 @@ This family lets an agent delegate work to child agents. Multiple named provider
| Package | Role | ctx key |
|---|---|---|
| [`subagent/`](subagent/README.md) | Defines provider registration, delegation, and continuation | `ctx.subagents` |
| [`subagent-inprocess/`](subagent-inprocess/README.md) | Provides the shared in-process run driver | — |
| [`subagent-spawn/`](subagent-spawn/README.md) | Starts a fresh in-process child | registers on `ctx.subagents` |
| [`subagent-fork/`](subagent-fork/README.md) | Starts an in-process child from the parent's completed history | registers on `ctx.subagents` |
| [`subagent-inprocess/`](subagent-in-process-driver/README.md) | Provides the shared in-process run driver | — |
| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.md) | Starts a fresh in-process child | registers on `ctx.subagents` |
| [`subagent-fork-in-process/`](subagent-fork-in-process/README.md) | Starts an in-process child from the parent's completed history | registers on `ctx.subagents` |
| [`subagent-acp/`](subagent-acp/README.md) | Starts an out-of-process child over ACP | registers on `ctx.subagents` |
| [`subagent-codex/`](subagent-codex/README.md) | Starts a real Codex app-server child | registers on `ctx.subagents` |
| [`subagent-claude-code/`](subagent-claude-code/README.md) | Starts a real Claude Code child through the official Claude Agent SDK | registers on `ctx.subagents` |

View File

@@ -7,9 +7,9 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
| [`subagent/`](subagent/README.md) | 定义提供方注册、委派和继续执行 | `ctx.subagents` |
| [`subagent-inprocess/`](subagent-inprocess/README.md) | 提供共享的进程内运行驱动器 | 无 |
| [`subagent-spawn/`](subagent-spawn/README.md) | 启动全新的进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-fork/`](subagent-fork/README.md) | 从父 agent 已完成的历史记录启动进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-inprocess/`](subagent-in-process-driver/README.md) | 提供共享的进程内运行驱动器 | 无 |
| [`subagent-spawn-in-process/`](subagent-spawn-in-process/README.md) | 启动全新的进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-fork-in-process/`](subagent-fork-in-process/README.md) | 从父 agent 已完成的历史记录启动进程内子 agent | 注册到 `ctx.subagents` |
| [`subagent-acp/`](subagent-acp/README.md) | 通过 ACPAgent Client Protocol启动进程外子 agent | 注册到 `ctx.subagents` |
| [`subagent-codex/`](subagent-codex/README.md) | 启动真实的 Codex app-server 子 agent | 注册到 `ctx.subagents` |
| [`subagent-claude-code/`](subagent-claude-code/README.md) | 通过官方 Claude Agent SDK 启动真实的 Claude Code 子 agent | 注册到 `ctx.subagents` |

View File

@@ -5,8 +5,8 @@ import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { resolveExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
import * as acp from '../src/index.ts'
@@ -52,8 +52,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
it('drives the real acp-agent example process to answer a prompt', async () => {
workdir = await mkdtemp(join(tmpdir(), 'dsh-subagent-acp-e2e-'))
ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: childLaunch.command,
@@ -82,8 +82,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
it('drives the child to do real file work via its own bash tool', async () => {
workdir = await mkdtemp(join(tmpdir(), 'dsh-subagent-acp-e2e-'))
ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: childLaunch.command,

View File

@@ -5,13 +5,13 @@ import { chmodSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type { SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
import * as acp from '../src/index.ts'
import { acpStopReason, acpContentText, DEFAULT_DISPOSE_EOF_GRACE_MS, DEFAULT_DISPOSE_GRACE_MS, disposeAcpChild, startAcpRun, toAcpPrompt, type AcpRunSpec } from '../src/run.ts'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { spawnSubprocess } from '@deepseek-ai/dsh-subprocess-local/src/spawn.ts'
/**
@@ -43,8 +43,8 @@ interface SetupEnv {
*/
async function setup(mockEnv: SetupEnv = {}, permission: 'allow' | 'reject' = 'reject') {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -222,8 +222,8 @@ describe('cwd resolution', () => {
const sentinel = join(tmp, 'spawned')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
// A command that would create the sentinel if the child were ever spawned.
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
const parent = { id: 'parent', session: { header: {} } } as unknown as Agent
@@ -241,8 +241,8 @@ describe('cwd resolution', () => {
const parentDir = realpathSync(mkdtempSync(join(tmpdir(), 'acp-parent-cwd-')))
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -269,8 +269,8 @@ describe('cwd resolution', () => {
const relative = 'packages/subagent/subagent-acp'
const absolute = resolve(relative)
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -289,8 +289,8 @@ describe('cwd resolution', () => {
// `path.resolve('')` is the process cwd, so an empty string would silently
// reintroduce the launch-directory fallback this resolution removed.
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
@@ -310,8 +310,8 @@ describe('cwd resolution', () => {
chmodSync(tmp, 0o600)
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
@@ -329,8 +329,8 @@ describe('cwd resolution', () => {
it('rejects a config cwd that is not an accessible directory at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, {
providerName: 'acp',
command: 'true',
@@ -371,8 +371,8 @@ describe('cwd resolution', () => {
const sentinel = join(tmp, 'spawned')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
const parent = { id: 'parent', session: { header: { cwd: join(tmp, 'vanished') } } } as unknown as Agent
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
@@ -694,8 +694,8 @@ describe('dsh-subagent-acp', () => {
const ready = join(tmp, 'trap-armed')
try {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: process.execPath,
@@ -727,8 +727,8 @@ describe('dsh-subagent-acp', () => {
{ disposeGraceMs: MAX_TIMER_DELAY_MS + 1 },
]) {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await expect(ctx.plugin(acp, { providerName: 'acp', command: 'true', args: [], permission: 'reject', env: {}, ...bad }))
.rejects.toThrow(new RegExp(`subagent-acp: dispose(?:Eof)?GraceMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`))
await ctx.fiber.dispose()
@@ -737,8 +737,8 @@ describe('dsh-subagent-acp', () => {
it('rejects a startup failure via the provider load path', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(acp, {
providerName: 'acp',
command: '/nonexistent/acp-agent-binary',
@@ -868,8 +868,8 @@ describe('dsh-subagent-acp', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const fiber = await ctx.plugin(acp, { providerName: 'acp', command: 'x', args: [], permission: 'reject', env: {} })
expect(ctx.subagents.list()).toEqual(['acp'])
await fiber.dispose()

View File

@@ -33,10 +33,10 @@
"path": "../../util/timeout"
},
{
"path": "../../support/loader-smoke"
"path": "../../test-support/loader-smoke"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -13,9 +13,9 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as claudeCode from '../src/index.ts'
const execFileAsync = promisify(execFile)
@@ -113,8 +113,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {

View File

@@ -19,9 +19,9 @@ import type {
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as claudeCode from '../src/index.ts'
import {
startMessagesFixture,
@@ -150,8 +150,8 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawnSpecs: SubprocessSpawnSpec[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)

View File

@@ -20,13 +20,13 @@ import {
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type {
SubprocessHandle,
SubprocessOutcome,
SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import * as claudeCode from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
@@ -296,8 +296,8 @@ describe('task admission and package contracts', () => {
it('registers one fixed descriptor, validates config, and unregisters on HMR', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const fiber = await ctx.plugin(claudeCode, {})
expect(ctx.subagents.getProvider('claude-code')).toMatchObject({
name: 'claude-code',
@@ -327,8 +327,8 @@ describe('task admission and package contracts', () => {
it('starts through the registered provider with its resolved config and diagnostics', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const child = fakeChild()
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
.mockImplementation(() => child.handle)

View File

@@ -31,7 +31,7 @@
"path": "../../util/timeout"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -14,9 +14,9 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import {
startDeepSeekResponsesBridge,
@@ -96,8 +96,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {

View File

@@ -14,9 +14,9 @@ import { promisify } from 'node:util'
import { Context } from '@deepseek-ai/cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import {
startResponsesFixture,
@@ -97,8 +97,8 @@ async function realHarness(script: readonly ResponsesBehavior[]): Promise<{
}
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const handles: SubprocessHandle[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {

View File

@@ -5,13 +5,13 @@ import { describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type {
SubprocessHandle,
SubprocessOutcome,
} from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as codex from '../src/index.ts'
import * as invariant from '../src/invariant.ts'
import {
@@ -287,8 +287,8 @@ describe('task admission and package contracts', () => {
it('registers one fixed descriptor, validates config, and unregisters on HMR', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const fiber = await ctx.plugin(codex, {})
const provider = ctx.subagents.getProvider('codex')!
expect(provider).toMatchObject({
@@ -316,8 +316,8 @@ describe('task admission and package contracts', () => {
it('requires a parent session cwd without suggesting unsupported config', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
await ctx.plugin(codex, {})
@@ -1014,8 +1014,8 @@ describe('run lifecycle and quiescence', () => {
it('uses the registered provider config and logs flattened errors', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(LocalSubprocessRuntime)
const child = fakeChild()
const spawn = vi.spyOn(ctx.subprocess, 'spawn').mockReturnValue(child.handle)
const warnings: string[] = []

View File

@@ -39,7 +39,7 @@
"path": "../../util/timeout"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -90,7 +90,7 @@ type ResolvedConfig = Required<Omit<Config, 'cwd' | 'maxTokens'>> & Pick<Config,
* child cannot honor `outputSchema`/`maxDepth`/`toolFilter`/`persona` (the
* service rejects a request needing any of them before `start` runs).
*/
class SdkProvider implements SubagentProvider {
class SdkSubagentProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
// Context contract: an out-of-process SDK child starts fresh — no parent conversation crosses the process boundary.
readonly inheritsParentContext = false
@@ -134,5 +134,5 @@ export function apply(ctx: Context, config: Config): void {
const validated: ResolvedConfig = configuredCwd === undefined
? resolved
: { ...resolved, cwd: configuredCwd }
ctx.subagents.registerProvider(new SdkProvider(validated.providerName, ctx, validated))
ctx.subagents.registerProvider(new SdkSubagentProvider(validated.providerName, ctx, validated))
}

View File

@@ -12,7 +12,7 @@ import { existsSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as sdk from '../src/index.ts'
import {
@@ -36,7 +36,7 @@ function request(text = 'p', signal = new AbortController().signal) {
/** Mount the SDK backend pointed at the fake runtime, scripted by `fakeEnv`. */
async function setup(fakeEnv: Record<string, string> = {}, config: Partial<sdk.Config> = {}) {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
// The Config type models the post-validation shape, so the default registry
// name is stated here; the Loader-composition fixture omits providerName and
// exercises the schemastery default end to end.
@@ -361,7 +361,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
it('registers under the configured provider name and unregisters on fiber dispose (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(sdk, {
providerName: 'sdk-hmr',
command: process.execPath,
@@ -385,7 +385,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
it('rejects non-positive timing bounds at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const base = { providerName: 'sdk', command: 'true', args: [], provider: 'p', model: 'm', env: {} }
await expect(ctx.plugin(sdk, { ...base, shutdownTimeoutMs: 0 })).rejects.toThrow('shutdownTimeoutMs must be a positive finite number')
await expect(ctx.plugin(sdk, { ...base, disposeEofGraceMs: -1 })).rejects.toThrow('disposeEofGraceMs must be a positive finite number')
@@ -397,7 +397,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
'rejects invalid maxTokens %s at load',
async (maxTokens) => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await expect(ctx.plugin(sdk, {
providerName: 'sdk',
command: 'true',
@@ -415,7 +415,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
'defensively rejects invalid maxTokens %s when apply is called directly',
async (maxTokens) => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
expect(() => { sdk.apply(ctx, {
providerName: 'sdk',
command: 'true',
@@ -434,7 +434,7 @@ describe('dsh-subagent-dsh-sdk provider', () => {
it('rejects an empty config cwd at load', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await expect(ctx.plugin(sdk, {
providerName: 'sdk',
command: 'true',

View File

@@ -36,13 +36,13 @@
"path": "../subagent"
},
{
"path": "../../support/loader-smoke"
"path": "../../test-support/loader-smoke"
},
{
"path": "../../subprocess/subprocess"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +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/subagent/subagent-fork/README.md
README.md: 2bd72058ab7d112f8f317a33842fc4d95b719017
README.zh.md: 40f9e34c5a8ae8c74de2ae0c9e4676866f0bd083
# pnpm run verify-translation-pairing --write packages/subagent/subagent-fork-in-process/README.md
README.md: 74c27ff10c76aa711ed3e954e806c00a27aacfa5
README.zh.md: bca890769bab1f898abd0b71fc8aa321b66e93c8

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-fork
# @deepseek-ai/dsh-subagent-fork-in-process
English | [中文](README.zh.md)
@@ -14,7 +14,7 @@ The seed transfers conversation history only. The child still receives a fresh f
## Start and capabilities
`start(request)` passes the completed-turn seed to [`startInProcessRun`](../subagent-inprocess/README.md) and awaits child publication. The shared driver owns cancellation, depth, customization, result reading, and disposal.
`start(request)` passes the completed-turn seed to [`startInProcessRun`](../subagent-in-process-driver/README.md) and awaits child publication. The shared driver owns cancellation, depth, customization, result reading, and disposal.
Fork advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`, identical to spawn.
@@ -23,7 +23,7 @@ Fork advertises `{ outputSchema: true, depthLimit: true, toolFilter: true, perso
| Key | Meaning |
|---|---|
| `providerName` | Registry name on `ctx.subagents` (default `fork`). |
See [`dsh-subagent-spawn`](../subagent-spawn/README.md) for the run lifecycle, model inheritance, and depth tracking — all shared.
See [`dsh-subagent-spawn-in-process`](../subagent-spawn-in-process/README.md) for the run lifecycle, model inheritance, and depth tracking — all shared.
## Model Experience

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-fork
# @deepseek-ai/dsh-subagent-fork-in-process
[English](README.md) | 中文
@@ -14,7 +14,7 @@ subagent 启动时,父 agent 当前的工具调用轮次仍未结束:其日
## 启动与能力
`start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-inprocess/README.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose资源释放
`start(request)` 将已完成轮次的初始内容传给 [`startInProcessRun`](../subagent-in-process-driver/README.md),并等待子 agent 发布。共享驱动器负责取消、深度、定制、结果读取和 dispose资源释放
fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona: true }`,与 spawn 相同。
@@ -23,7 +23,7 @@ fork 声明 `{ outputSchema: true, depthLimit: true, toolFilter: true, persona:
| 键 | 含义 |
|---|---|
| `providerName` | `ctx.subagents` 上的注册表名称(默认 `fork`)。 |
运行生命周期、模型继承与深度跟踪均为共享行为,见 [`dsh-subagent-spawn`](../subagent-spawn/README.md)。
运行生命周期、模型继承与深度跟踪均为共享行为,见 [`dsh-subagent-spawn-in-process`](../subagent-spawn-in-process/README.md)。
## 模型体验

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-subagent-fork",
"name": "@deepseek-ai/dsh-subagent-fork-in-process",
"description": "In-process fork subagent backend: runs a child agent seeded with a prefix of the parent's log",
"version": "0.0.1-rc.2",
"publishConfig": {
@@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-fork"
"directory": "packages/subagent/subagent-fork-in-process"
},
"type": "module",
"main": "lib/index.js",
@@ -36,7 +36,7 @@
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
@@ -51,8 +51,8 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -4,7 +4,7 @@
* parent's session log — so the child inherits the parent's conversation context instead of
* starting fresh. The seed ends at the last `turn/end`: the current tool-call turn is
* unbalanced and cannot be replayed as a valid child session.
* @module @deepseek-ai/dsh-subagent-fork
* @module @deepseek-ai/dsh-subagent-fork-in-process
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -18,10 +18,10 @@ import type {
SubagentCapabilities,
SubagentProvider,
} from '@deepseek-ai/dsh-subagent'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-in-process-driver'
export const name = 'subagent-fork'
// `tools` is deliberately NOT injected — same rationale as subagent-spawn: the
export const name = 'subagent-fork-in-process'
// `tools` is deliberately NOT injected — same rationale as subagent-spawn-in-process: the
// per-run structured runtime gates its capture-tool registration on `tools`
// itself, so this backend's apply timing (and the delegation tool's position
// in the model-visible tool list) is unchanged by structured output.
@@ -58,7 +58,7 @@ function completedTurnPrefix(parent: Agent): SessionEvent[] {
* in-process structured runtime), plus `toolFilter`/`persona` (scoped
* restrict() and a scoped shadowing persona section).
*/
class ForkProvider implements SubagentProvider {
class ForkInProcessProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }
// Context contract: a forked child IS seeded with the parent's completed-turn prefix.
readonly inheritsParentContext = true
@@ -90,5 +90,5 @@ class ForkProvider implements SubagentProvider {
}
export function apply(ctx: Context, config: Config): void {
ctx.subagents.registerProvider(new ForkProvider(config.providerName))
ctx.subagents.registerProvider(new ForkInProcessProvider(config.providerName))
}

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-fork`.
* @module @deepseek-ai/dsh-subagent-fork/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-fork-in-process`.
* @module @deepseek-ai/dsh-subagent-fork-in-process/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-fork-in-process'
/** Cordis companion plugin name. */
export const name = 'subagent-fork-invariant'
export const name = 'subagent-fork-in-process-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -4,19 +4,19 @@ import { Context } from '@deepseek-ai/cordis'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as fork from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -37,7 +37,7 @@ async function setup(script: Script) {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(Spawn, { providerName: 'spawn' })
await ctx.plugin(fork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))

View File

@@ -6,20 +6,20 @@ import AgentRegistry from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
import * as fork from '../src/index.ts'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -44,7 +44,7 @@ async function setup(script: Script) {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(fork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -55,7 +55,7 @@ function text(blocks: { type: string; text?: string }[]): string {
return blocks.filter(b => b.type === 'text').map(b => b.text).join('')
}
describe('dsh-subagent-fork', () => {
describe('dsh-subagent-fork-in-process', () => {
it('emits subagent/start only after the seeded child is published', async () => {
const { ctx, parent } = await setup([textResponse('child answer')])
let childAtStart: ReturnType<typeof ctx.agents.get>
@@ -201,7 +201,7 @@ describe('dsh-subagent-fork', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(AgentRegistry)
const fiber = await ctx.plugin(fork, { providerName: 'fork' })
expect(ctx.subagents.list()).toEqual(['fork'])
@@ -240,12 +240,12 @@ describe('dsh-subagent-fork', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in fork).toBe(false)
expect(fork.name).toBe('subagent-fork')
expect(fork.name).toBe('subagent-fork-in-process')
expect(fork.inject).toEqual(['subagents'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(fork) as Record<string, unknown>
expect(unwrapped).toBe(fork)
expect(unwrapped.name).toBe('subagent-fork')
expect(unwrapped.name).toBe('subagent-fork-in-process')
expect(unwrapped.inject).toEqual(['subagents'])
expect(typeof unwrapped.apply).toBe('function')
})

View File

@@ -27,10 +27,10 @@
"path": "../subagent"
},
{
"path": "../subagent-inprocess"
"path": "../subagent-in-process-driver"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +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/subagent/subagent-spawn/README.md
README.md: 811f19e6e68362bd14e75d0a9059ee61fda3f015
README.zh.md: 2736143214039daa3129fd114d4294dc5fcb7d5e
# pnpm run verify-translation-pairing --write packages/subagent/subagent-in-process-driver/README.md
README.md: 47a5c09fc1c80c5dc3062be82e7355b874a627d3
README.zh.md: bcd6a2cf31c351722dfefe55e8b8ff75d4252c40

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-inprocess
# @deepseek-ai/dsh-subagent-in-process-driver
English | [中文](README.zh.md)

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-inprocess
# @deepseek-ai/dsh-subagent-in-process-driver
[English](README.md) | 中文

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-subagent-inprocess",
"name": "@deepseek-ai/dsh-subagent-in-process-driver",
"description": "Shared in-process subagent run driver: drives a child agent on ctx.agents (used by the spawn and fork backends)",
"version": "0.0.1-rc.2",
"publishConfig": {
@@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-inprocess"
"directory": "packages/subagent/subagent-in-process-driver"
},
"type": "module",
"main": "lib/index.js",

View File

@@ -8,7 +8,7 @@
* composes and drives them directly, so this driver owns exactly one turn with
* one result.
*
* @module @deepseek-ai/dsh-subagent-inprocess
* @module @deepseek-ai/dsh-subagent-in-process-driver
*/
import { randomUUID } from 'node:crypto'

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-spawn`.
* @module @deepseek-ai/dsh-subagent-spawn/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-in-process-driver`.
* @module @deepseek-ai/dsh-subagent-in-process-driver/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-in-process-driver'
/** Cordis companion plugin name. */
export const name = 'subagent-spawn-invariant'
export const name = 'subagent-in-process-driver-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -7,7 +7,7 @@
* Capture commits only after the authoritative `tools/result` succeeds; Code Mode capture also
* waits for the enclosing `run_code` result. The terminal result marker and monotonic tool
* guard prevent later calls from reopening a completed structured run.
* @module @deepseek-ai/dsh-subagent-inprocess/structured
* @module @deepseek-ai/dsh-subagent-in-process-driver/structured
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -88,7 +88,7 @@ export function attachStructuredRuntime(childCtx: Context, schema: ObjectJsonSch
// within the same turn, exactly like a schema-validated defineTool call.
if (violations.length > 0) throw new ToolArgsError(violations)
// Two-phase commit, keyed by THIS execution: later transformable
// waterfalls may still turn the success into an error. ToolRegistry has
// waterfalls may still turn the success into an error. ToolRuntime has
// already frozen model-bound arguments at the actual input boundary.
staged.set(exec, { value: args })
exec.concludeTurn()

View File

@@ -4,11 +4,11 @@ import { createUserMessage, CallId, type ContentBlock, type GenerateOptions } fr
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, {
import SubagentRuntime, {
type ResolvedSubagentStartRequest,
type SubagentStartRequest,
} from '@deepseek-ai/dsh-subagent'
@@ -26,7 +26,7 @@ const testToolSignal = new AbortController().signal
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -67,7 +67,7 @@ async function setup(script: Script, options: SetupOptions = {}) {
}
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const disposeProvider = ctx.subagents.registerProvider({
name: 'spawn',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: false, persona: false },

View File

@@ -5,11 +5,11 @@ import { type Agent, type AgentOptions } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { maxTokensResponse, MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import { startInProcessRun } from '../src/index.ts'
@@ -17,7 +17,7 @@ import { startInProcessRun } from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -28,7 +28,7 @@ async function setup(script: Script, parentOptions: Partial<AgentOptions> = {})
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const adapter = new MockAdapter(script)
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock', ...parentOptions })

View File

@@ -33,7 +33,7 @@
"path": "../../core/tools"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -1,6 +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/subagent/subagent-inprocess/README.md
README.md: 69def8bf8f41e3685d017ac4b003b26a37f064ef
README.zh.md: bf5e7cb5cc8517ee7020695ef10e3b58d613541b
# pnpm run verify-translation-pairing --write packages/subagent/subagent-spawn-in-process/README.md
README.md: f1fb96f2230359cb3ff55c630f29fd34345dbed7
README.zh.md: 940701d6fac78ce75fda24519fa22b268085ea1a

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-spawn
# @deepseek-ai/dsh-subagent-spawn-in-process
English | [中文](README.zh.md)
@@ -6,7 +6,7 @@ The spawn provider creates a fresh child `Agent` in the current process. The chi
## Behavior
`start(request)` delegates to [`startInProcessRun`](../subagent-inprocess/README.md) with no seed and awaits publication before returning. The child receives parent working-directory/session lineage and inherits the parent model unless overridden, but starts with an empty conversation.
`start(request)` delegates to [`startInProcessRun`](../subagent-in-process-driver/README.md) with no seed and awaits publication before returning. The child receives parent working-directory/session lineage and inherits the parent model unless overridden, but starts with an empty conversation.
The shared driver owns depth checking, persona and tool-filter setup, structured output, required-signal cancellation, one-shot execution, result reading, and quiescent disposal. A startup rejection leaves no published child; provider unload after fulfillment does not revoke the holder-owned run.

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-subagent-spawn
# @deepseek-ai/dsh-subagent-spawn-in-process
[English](README.md) | 中文
@@ -6,7 +6,7 @@ spawn 提供方会在当前进程中创建一个全新的子 `Agent`。子 agent
## 行为
`start(request)` 不传入 seed直接委托给 [`startInProcessRun`](../subagent-inprocess/README.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。
`start(request)` 不传入 seed直接委托给 [`startInProcessRun`](../subagent-in-process-driver/README.md),并在子 agent 发布后才返回。子 agent 获得父 agent 的工作目录/会话谱系,并默认继承父 agent 模型(除非覆盖),但以空对话开始运行。
共享驱动器负责深度检查、persona 与工具过滤器设置、结构化输出、通过必需的信号执行取消、单次执行、结果读取和完全停稳后的 dispose资源释放。启动遭拒不会留下已发布的子 agent启动调用兑现后卸载提供方也不会撤销由持有方拥有的运行。

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-subagent-spawn",
"name": "@deepseek-ai/dsh-subagent-spawn-in-process",
"description": "In-process spawn subagent backend: runs a fresh child agent on ctx.agents",
"version": "0.0.1-rc.2",
"publishConfig": {
@@ -8,7 +8,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/subagent/subagent-spawn"
"directory": "packages/subagent/subagent-spawn-in-process"
},
"type": "module",
"main": "lib/index.js",
@@ -34,7 +34,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
@@ -52,7 +52,7 @@
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-inprocess": "workspace:^",
"@deepseek-ai/dsh-subagent-in-process-driver": "workspace:^",
"@deepseek-ai/dsh-tool-bash": "workspace:^",
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -3,7 +3,7 @@
* `ctx.subagents` that runs each child as a fresh child {@link Agent} on the same cordis
* context (its own session, own system prompt, zero parent context). The cheapest transport,
* reusing the agent factory's quiescent teardown.
* @module @deepseek-ai/dsh-subagent-spawn
* @module @deepseek-ai/dsh-subagent-spawn-in-process
*/
import type { Context } from '@deepseek-ai/cordis'
@@ -14,9 +14,9 @@ import type {
SubagentCapabilities,
SubagentProvider,
} from '@deepseek-ai/dsh-subagent'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-inprocess'
import { startInProcessRun } from '@deepseek-ai/dsh-subagent-in-process-driver'
export const name = 'subagent-spawn'
export const name = 'subagent-spawn-in-process'
// `tools` is deliberately not injected: the child factory already provides it during setup,
// and adding it here would unnecessarily change this provider's apply timing.
export const inject = ['subagents']
@@ -38,7 +38,7 @@ export const Config: z<Config> = z.object({
* `restrict()` and a scoped shadowing persona section, applied in the child's
* creation window).
*/
class SpawnProvider implements SubagentProvider {
class SpawnInProcessProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }
// Context contract: a spawned child starts fresh — it never sees the parent conversation.
readonly inheritsParentContext = false
@@ -60,5 +60,5 @@ class SpawnProvider implements SubagentProvider {
}
export function apply(ctx: Context, config: Config): void {
ctx.subagents.registerProvider(new SpawnProvider(config.providerName))
ctx.subagents.registerProvider(new SpawnInProcessProvider(config.providerName))
}

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-inprocess`.
* @module @deepseek-ai/dsh-subagent-inprocess/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-subagent-spawn-in-process`.
* @module @deepseek-ai/dsh-subagent-spawn-in-process/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-inprocess'
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-spawn-in-process'
/** Cordis companion plugin name. */
export const name = 'subagent-insubprocess-invariant'
export const name = 'subagent-spawn-in-process-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -3,11 +3,11 @@ import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import * as BashEnvPlugin from '@deepseek-ai/dsh-bash-env'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as Spawn from '../src/index.ts'
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
@@ -29,11 +29,11 @@ export async function spawnHarness(workdir: string): Promise<Context> {
})
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(LlmDeepSeek)
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(LocalSubprocessRuntime)
await ctx.plugin(BashEnvPlugin)
await ctx.plugin(LocalBashExecutor, { cwd: workdir, timeoutMs: 30_000 })
await ctx.plugin(ToolBash)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(Spawn, { providerName: 'spawn' })
// The model-facing subagent tool, bound to the spawn backend.
await ctx.plugin(ToolSubagent, { provider: 'spawn' })

View File

@@ -6,20 +6,20 @@ import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as spawn from '../src/index.ts'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver'
import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
type Script = ConstructorParameters<typeof MockAdapter>[0]
async function mountInvariants(ctx: Context): Promise<void> {
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SessionInvariant)
await ctx.plugin(AgentInvariant)
await ctx.plugin(AgentLoopInvariant)
@@ -27,7 +27,7 @@ async function mountInvariants(ctx: Context): Promise<void> {
/**
* Drives the REAL spawn backend end-to-end: a real agent loop + a scripted mock
* MODEL (the only mocked boundary) + the real SubagentService + the real
* MODEL (the only mocked boundary) + the real SubagentRuntime + the real
* invariant service plus package companions (so a malformed child session log would fail the test).
* The parent is a real config agent; the spawn provider creates a real child
* agent on the same context and we assert its output.
@@ -38,7 +38,7 @@ async function setup(script: Script) {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(spawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -64,7 +64,7 @@ function disposeChildLifecycle(parent: Agent): void {
void lifecycle()
}
describe('dsh-subagent-spawn', () => {
describe('dsh-subagent-spawn-in-process', () => {
it('runs a fresh child to completion and returns its final assistant output', async () => {
// One model call for the child: a plain text answer.
const { ctx, parent } = await setup([textResponse('child answer')])
@@ -290,7 +290,7 @@ describe('dsh-subagent-spawn', () => {
it('unregisters the provider when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(AgentRegistry)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
expect(ctx.subagents.list()).toEqual(['spawn'])
@@ -322,7 +322,7 @@ describe('dsh-subagent-spawn', () => {
await mountAgentLoopTestDependencies(ctx)
await mountInvariants(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -350,7 +350,7 @@ describe('dsh-subagent-spawn', () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(spawn, { providerName: 'spawn' })
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
const parentEffects = parent.ctx.fiber.getEffects().length
@@ -370,12 +370,12 @@ describe('dsh-subagent-spawn', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in spawn).toBe(false)
expect(spawn.name).toBe('subagent-spawn')
expect(spawn.name).toBe('subagent-spawn-in-process')
expect(spawn.inject).toEqual(['subagents'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(spawn) as Record<string, unknown>
expect(unwrapped).toBe(spawn)
expect(unwrapped.name).toBe('subagent-spawn')
expect(unwrapped.name).toBe('subagent-spawn-in-process')
expect(unwrapped.inject).toEqual(['subagents'])
expect(typeof unwrapped.apply).toBe('function')
})

View File

@@ -21,10 +21,10 @@
"path": "../subagent"
},
{
"path": "../subagent-inprocess"
"path": "../subagent-in-process-driver"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -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/subagent/subagent/README.md
README.md: edddb39a2c197eb7c8acb27f5b7f6b8c6016c517
README.zh.md: 65ded637beb296bb73266ff06a4e7fadbae3b2c8
README.md: 3a0142ed7e72d4f276dd475e1a8e1fa5e3aab9a6
README.zh.md: 13ec9f61a28f425bbf8c80feed2e6211b2fe7f16

View File

@@ -8,7 +8,7 @@ The [subagent family overview](../README.md) maps implementations and model-faci
## Service API
`SubagentService` has these operations:
`SubagentRuntime` has these operations:
| Member | Meaning |
|---|---|
@@ -94,7 +94,7 @@ Run events are scoped to the delegating parent. Every listener is independently
Provider additions and removals also emit `subagent/provider-added` and `subagent/provider-removed`. Consumers such as the model-facing tool use those events because Cordis may load sibling plugins concurrently; configuration order does not prove registration order.
Continuable children do not create `SubagentRun` or Tasks. The continuation manager directly owns one process-local Activation and retained `AgentHandle` per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable `parentSession`, and `MessageSource` records the sender without granting authority. Interrupt authority is deliberately wider than delivery authority: a human presents the durable direct-parent address so a live child stays stoppable while its parent Agent is offline, and any exact live ancestor recorded in the Activation's materialization lineage may stop its descendant, because stopping a turn is idempotent and delivers no content.
Continuable children do not create `SubagentRun` or Jobs. The continuation manager directly owns one process-local Activation and retained `AgentHandle` per resident child Session, uses the Agent inbox as the only FIFO, and cold-resumes from the durable descriptor. Exact live direct-parent identity authorizes parent-to-child delivery. Exact live child identity authorizes reports; the manager derives the recipient from durable `parentSession`, and `MessageSource` records the sender without granting authority. Interrupt authority is deliberately wider than delivery authority: a human presents the durable direct-parent address so a live child stays stoppable while its parent Agent is offline, and any exact live ancestor recorded in the Activation's materialization lineage may stop its descendant, because stopping a turn is idempotent and delivers no content.
When `ctx.sessionProjections` is available, the service registers two projection units. `subagentTiming` resets at each descriptor so a fork seed's ancestor work cannot enter the child's total, then accumulates `turn/start``turn/end` active time and retains same-cut `active.since` and `active.through` bounds for an open turn; while that turn remains open, `active.through` follows the latest folded event, giving an inactive consumer a conservative crash bound without mixing in newer session metadata. `subagent` folds the durable identity — mode plus creation label — from `subagent/descriptor` events with the same last-wins reset discipline, so a fork seed's ancestor descriptor stands only until the child's own overrides it; a malformed or unrecognized-version payload folds to the serializable `null` sentinel — indistinguishable from a log with no descriptor, and surviving every JSON push frame so a consumer replaces a stale identity instead of keeping it — and never throws.
@@ -131,7 +131,7 @@ Every in-process child's runtime-context snapshot carries the `subagent:delegati
##### The delegation-scope statement
```markdown
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the job needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
```
#### Token effect

View File

@@ -8,7 +8,7 @@ subagent seam 允许一个 agent智能体通过具名提供方把工作委
## 服务 API
`SubagentService` 具有以下操作:
`SubagentRuntime` 具有以下操作:
| 成员 | 含义 |
|---|---|
@@ -131,7 +131,7 @@ subagent seam 允许一个 agent智能体通过具名提供方把工作委
##### 委派范围声明
```markdown
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the job needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.
```
#### Token 影响

View File

@@ -52,7 +52,7 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
@@ -76,7 +76,7 @@
"@deepseek-ai/dsh-session-projection-cache": {
"optional": true
},
"@deepseek-ai/dsh-tasks": {
"@deepseek-ai/dsh-jobs": {
"optional": true
},
"@deepseek-ai/dsh-user-approval": {
@@ -98,7 +98,7 @@
"@deepseek-ai/dsh-session-projection-cache": "workspace:^",
"@deepseek-ai/dsh-storage": "workspace:^",
"@deepseek-ai/dsh-storage-domain": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"

View File

@@ -161,7 +161,7 @@ type ActivationState = 'running' | 'waiting' | 'settled'
/**
* Hooks the manager needs from the owning service. Declared here, by the
* dependent, so the manager states exactly what it requires instead of
* depending back on the whole {@link SubagentService}. Package-private: no
* depending back on the whole {@link SubagentRuntime}. Package-private: no
* consumer outside this package supplies a host.
*/
interface ContinuationHost {

View File

@@ -7,10 +7,10 @@
* Unlike the bash seam (one executor per context, second load throws), MULTIPLE
* providers coexist here: each registers under a unique name and a caller picks
* one by name. The shape mirrors the LLM adapter registry
* (`LlmService.registerAdapter`), not the single-service bash executor.
* (`LlmRuntime.registerAdapter`), not the single-service bash executor.
*
* This package owns the Service Definition role of the capability seam. Service providers
* (`@deepseek-ai/dsh-subagent-spawn`, `-fork`, `-acp`) and the model-facing
* (`@deepseek-ai/dsh-subagent-spawn-in-process`, `-fork`, `-acp`) and the model-facing
* consumer (`@deepseek-ai/dsh-tool-subagent`) are separate packages.
*
* Public operations express caller intent: `start` returns one published owned
@@ -128,7 +128,7 @@ export type { SubagentIdentityProjection, SubagentTimingProjection } from './pro
declare module '@deepseek-ai/cordis' {
interface Context {
subagents: SubagentService
subagents: SubagentRuntime
}
interface Events {
@@ -154,7 +154,7 @@ declare module '@deepseek-ai/cordis' {
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
'subagent/start'(this: Scoped<SubagentRuntime>, info: SubagentRunInfo): void
/**
* A published child settled. Scope-filtered dispatch uses the same delegating
* parent carrier as `subagent/start`, so the lifecycle pair reaches the
@@ -163,12 +163,12 @@ declare module '@deepseek-ai/cordis' {
* @dshScopeScan unsupported
* @mode emit
*/
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
'subagent/end'(this: Scoped<SubagentRuntime>, info: SubagentRunEndInfo): void
}
}
/** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
export class SubagentService extends Service {
export class SubagentRuntime extends Service {
private providers = new Map<string, SubagentProvider>()
private continuations: SubagentContinuationManager | undefined
/** Deployment contributions composed into unpublished continuable children. */
@@ -369,7 +369,7 @@ export class SubagentService extends Service {
registerProvider(provider: SubagentProvider): () => void {
const name = provider.name
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; direct return preserves disposer identity
return this.ctx.effect(function* (this: SubagentService) {
return this.ctx.effect(function* (this: SubagentRuntime) {
if (this.providers.has(name)) {
throw new SubagentError(`a subagent provider named "${name}" is already registered`, 'DUPLICATE_PROVIDER')
}
@@ -496,4 +496,4 @@ export class SubagentService extends Service {
}
}
export default SubagentService
export default SubagentRuntime

View File

@@ -122,7 +122,7 @@ interface PositionedCandidate {
* projection-cache row when it serves an own-suffix identity (the seq gate),
* else one bounded-concurrency persistence inspection folded through the
* registry.
* @see SubagentService.listChildren for the public cancellation and failure contract.
* @see SubagentRuntime.listChildren for the public cancellation and failure contract.
* @param ctx - context carrying the session store, the projection registry,
* optional persistence, and the optional projection cache.
* @param parentSessionId - parent session whose direct children are listed.
@@ -151,7 +151,7 @@ export async function listChildren(
* continuable child below either is still discovered. Classification uses the
* same projection-backed runtime as {@link listChildren}; no Agent is loaded or
* resumed.
* @see SubagentService.listDescendants for the public cancellation and failure contract.
* @see SubagentRuntime.listDescendants for the public cancellation and failure contract.
* @param ctx - context carrying the session store, projection registry, and optional persistence/cache.
* @param rootSessionId - session whose complete descendant tree is listed.
* @param signal - caller-owned cancellation observed around every persistence read.

View File

@@ -1,13 +1,13 @@
/**
* Settlement of one ONE-SHOT subagent run into a background-Task outcome. Only
* the one-shot background path uses Tasks; continuable children have no Task,
* the one-shot background path uses Jobs; continuable children have no Task,
* no per-message result, and no Task cancellation.
*
* @module @deepseek-ai/dsh-subagent/run-settlement
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { TaskOutcome } from '@deepseek-ai/dsh-tasks'
import type { JobOutcome } from '@deepseek-ai/dsh-jobs'
import type { SubagentResult, SubagentRun } from './types.ts'
/** Flatten a child's final output blocks to the task's final text. */
@@ -22,9 +22,9 @@ function finalText(blocks: ContentBlock[]): string {
* Map a child result to the task outcome: completed carries final text,
* aborted is killed, and every other reason is failed without partial output.
* @param result - child terminal result.
* @returns outcome for the `ctx.tasks` registration.
* @returns outcome for the `ctx.jobs` registration.
*/
function runOutcome(result: SubagentResult): TaskOutcome {
function runOutcome(result: SubagentResult): JobOutcome {
switch (result.stopReason) {
case 'completed':
return { status: 'completed', output: finalText(result.output) }
@@ -46,8 +46,8 @@ function runOutcome(result: SubagentResult): TaskOutcome {
* @param run - live run to settle and release.
* @returns outcome after child resources are released.
*/
export async function settleRun(run: SubagentRun): Promise<TaskOutcome> {
let outcome: TaskOutcome
export async function settleRun(run: SubagentRun): Promise<JobOutcome> {
let outcome: JobOutcome
try {
outcome = runOutcome(await run.result)
} catch (error: unknown) {

View File

@@ -149,7 +149,7 @@ export interface SubagentStartRequest {
}
/**
* Provider-facing one-shot request after {@link SubagentService.start} resolves
* Provider-facing one-shot request after {@link SubagentRuntime.start} resolves
* the durable child descriptor.
*/
export interface ResolvedSubagentStartRequest extends SubagentStartRequest {

View File

@@ -19,11 +19,11 @@ import SandboxPolicyService, { effectiveSandboxMode, setSandboxMode } from '@dee
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import ApprovalService, { effectiveApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SubagentService from '../src/index.ts'
import SubagentRuntime from '../src/index.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
@@ -45,7 +45,7 @@ async function setup(script: Script) {
await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write', workspaceRoot: root })
await ctx.plugin(ApprovalService)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))

View File

@@ -9,14 +9,14 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import type { GenerateOptions, MessageId, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
import { defineTool } from '@deepseek-ai/dsh-tools'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import SubagentService, {
import SubagentRuntime, {
SubagentError,
SUBAGENT_DESCRIPTOR_VERSION,
} from '../src/index.ts'
@@ -69,7 +69,7 @@ async function setupWith(adapter: LlmAdapter, options: { persistence?: boolean }
disposePersistence = () => persistenceFiber.dispose()
}
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -167,7 +167,7 @@ function observeCancel(agent: Agent, callback: () => void): void {
})
}
describe('SubagentService.startContinuable', () => {
describe('SubagentRuntime.startContinuable', () => {
it('returns both identities at inbox acceptance, without waiting for the turn or the log', async () => {
const { ctx, parent, adapter } = await setup([textResponse('first answer')])
const enqueued: { id: MessageId; loggedYet: boolean }[] = []
@@ -374,7 +374,7 @@ describe('SubagentService.startContinuable', () => {
await mountAgentLoopTestDependencies(fresh)
await fresh.plugin(JsonlSessionPersistence, { root: root! })
await fresh.plugin(AgentLoop, { agents: [] })
await fresh.plugin(SubagentService)
await fresh.plugin(SubagentRuntime)
await fresh.plugin(SubagentSpawn, { providerName: 'spawn' })
const freshParent = fresh.agentLoop.create(SessionId('routeless-resume'), {})
await followup(fresh, freshParent, started.childId, message('resume routeless'))
@@ -436,7 +436,7 @@ describe('SubagentService.startContinuable', () => {
})
})
describe('SubagentService.followup residency routing', () => {
describe('SubagentRuntime.followup residency routing', () => {
it('enqueues in the same Activation while it is running, preserving one inbox FIFO', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([
@@ -480,7 +480,7 @@ describe('SubagentService.followup residency routing', () => {
it('cold-resumes after the initial provider unregisters', async () => {
const { ctx, parent } = await setup([textResponse('first'), textResponse('after resume')])
await ctx.plugin(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SubagentInvariant)
const disposeProvider = ctx.subagents.registerProvider({
name: 'retired',
@@ -2287,7 +2287,7 @@ describe('continuable errors', () => {
roots.push(root)
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
const serviceFiber = await ctx.plugin(SubagentService)
const serviceFiber = await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
@@ -2302,7 +2302,7 @@ describe('continuable errors', () => {
})
})
describe('SubagentService.interrupt', () => {
describe('SubagentRuntime.interrupt', () => {
it('aborts the current turn durably, parks accepted follow-ups, and resumes them only on a waking send', async () => {
const releaseFirst = Promise.withResolvers<undefined>()
const adapter = new GatedAdapter([

View File

@@ -2,19 +2,19 @@ import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentService, { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { SubagentRunId } from '@deepseek-ai/dsh-subagent'
import type {
SubagentProvider,
SubagentRunEndInfo,
SubagentRunInfo,
} from '@deepseek-ai/dsh-subagent'
import * as SubagentInvariant from '@deepseek-ai/dsh-subagent/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(InvariantService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(SubagentInvariant)
return ctx
}

View File

@@ -16,12 +16,12 @@ import SessionProjectionCache from '@deepseek-ai/dsh-session-projection-cache'
import Storage from '@deepseek-ai/dsh-storage'
import { DomainFacility } from '@deepseek-ai/dsh-storage-domain'
import { MemoryMediaPool, MemoryStorageBackend } from '../../../storage/storage-domain/tests/helpers/memory-backend.ts'
import SubagentService, {
import SubagentRuntime, {
SUBAGENT_DESCRIPTOR_VERSION,
SubagentError,
} from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as SubagentFork from '@deepseek-ai/dsh-subagent-fork-in-process'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
type Script = ConstructorParameters<typeof MockAdapter>[0]
@@ -51,7 +51,7 @@ async function setup(
ctx.provide('storageDomain', facility)
await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 })
}
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(SubagentFork, { providerName: 'fork' })
ctx.llm.registerAdapter(['mock'], new MockAdapter(script))
@@ -145,13 +145,13 @@ const hostileProjectionDefinition: ProjectionDefinition<'subagentListHostileProb
stateVersion: 1,
}
describe('SubagentService.listChildren', () => {
describe('SubagentRuntime.listChildren', () => {
it('lists live children without persistence, query services, or the continuation runtime', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
expect(ctx.get('tasks')).toBeUndefined()
await ctx.plugin(SubagentRuntime)
expect(ctx.get('jobs')).toBeUndefined()
expect(ctx.get('agents')).toBeUndefined()
expect(ctx.get('sessionPersistence')).toBeUndefined()
@@ -184,7 +184,7 @@ describe('SubagentService.listChildren', () => {
it('fails loud when the session store is not mounted', async () => {
const ctx = new Context()
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await expect(ctx.subagents.listChildren(SessionId('no-store-parent'))).rejects.toThrow(
expect.objectContaining({ code: 'SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE' }) as Error,
)
@@ -973,7 +973,7 @@ describe('SubagentService.listChildren', () => {
})
})
describe('SubagentService.listDescendants', () => {
describe('SubagentRuntime.listDescendants', () => {
it('flattens the complete tree in stable pre-order with verified parent and depth', async () => {
const { ctx, parent } = await setup([])
const childA = await authorChild(ctx, '00000000-0000-4000-8000-00000000aaa1', {

View File

@@ -4,7 +4,7 @@ import { type Agent } from '@deepseek-ai/dsh-agent'
import { HarnessError } from '@deepseek-ai/dsh-llm'
import { carrierKeyOf } from '@deepseek-ai/dsh-scope'
import SubagentService, {
import SubagentRuntime, {
foldSubagentDescriptor,
snapshotSubagentDescriptor,
SUBAGENT_DESCRIPTOR_VERSION,
@@ -62,13 +62,13 @@ class StubProvider implements SubagentProvider {
}
}
async function service(): Promise<{ ctx: Context; subagents: SubagentService }> {
async function service(): Promise<{ ctx: Context; subagents: SubagentRuntime }> {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
return { ctx, subagents: ctx.subagents }
}
describe('SubagentService', () => {
describe('SubagentRuntime', () => {
it('registers, lists, looks up, starts, and removes providers', async () => {
const { ctx, subagents } = await service()
const added: string[] = []
@@ -122,7 +122,7 @@ describe('SubagentService', () => {
},
})
expect(provider.lastRequest).not.toBe(request)
expectTypeOf<Parameters<SubagentService['start']>[1]>().toExtend<SubagentStartRequest>()
expectTypeOf<Parameters<SubagentRuntime['start']>[1]>().toExtend<SubagentStartRequest>()
expect('resume' in subagents).toBe(false)
expect('resume' in provider).toBe(false)
})

View File

@@ -3,7 +3,7 @@ import { Context } from '@deepseek-ai/cordis'
import SessionStore from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentService from '../src/index.ts'
import SubagentRuntime from '../src/index.ts'
import { subagentTimingProjectionDefinition } from '../src/projection.ts'
function event(type: SessionEvent['type'], seq: number, time: number): SessionEvent {
@@ -21,7 +21,7 @@ describe('subagent timing projection', () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SessionProjectionRegistry)
const serviceFiber = await ctx.plugin(SubagentService)
const serviceFiber = await ctx.plugin(SubagentRuntime)
const before = ctx.sessionProjections.snapshot(ctx.sessions.create()).values
expect(before.subagentTiming).toEqual({ settledMs: 0 })

View File

@@ -48,10 +48,10 @@
"path": "../../session/session-projection-cache"
},
{
"path": "../../tasks/tasks"
"path": "../../jobs/jobs"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -55,7 +55,7 @@
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -9,9 +9,9 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentListEntry } from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -59,7 +59,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -247,7 +247,7 @@ describe('dsh-tool-subagent-control/list-agents', () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(tool)
expect(ctx.tools.schemas().some(schema => schema.name === 'list_agents')).toBe(true)
await fiber.dispose()

View File

@@ -9,8 +9,8 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test
import { SessionId } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
@@ -58,7 +58,7 @@ async function setupWith(adapter: MockAdapter | GatedAdapter) {
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionProjectionRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(tool)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -107,8 +107,8 @@ describe('dsh-tool-subagent-control', () => {
const props = (schemas[0]!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['message', 'subagent_id'])
// The continuable path has no Task, so the schema must not promise one.
expect(schemas[0]!.description).not.toContain('task_output')
expect(schemas[0]!.description).not.toContain('task id')
expect(schemas[0]!.description).not.toContain('job_output')
expect(schemas[0]!.description).not.toContain('job id')
// Follow-up ordering is model-visible: it cannot redirect the open turn.
expect(schemas[0]!.description).toContain('next turn')
})
@@ -207,7 +207,7 @@ describe('dsh-tool-subagent-control', () => {
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(tool)
expect(ctx.tools.schemas().some(schema => schema.name === 'send_message')).toBe(true)
expect(ctx.tools.schemas().some(schema => schema.name === 'interrupt_agent')).toBe(true)

View File

@@ -27,7 +27,7 @@
"path": "../subagent"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -52,7 +52,7 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tool-subagent-control": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",

View File

@@ -12,8 +12,8 @@ import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEvent } from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as control from '@deepseek-ai/dsh-tool-subagent-control'
import { textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as tool from '../src/index.ts'
@@ -51,7 +51,7 @@ async function setup(options: { load?: boolean; config?: tool.Config } = {}) {
const root = mkdtempSync(join(tmpdir(), 'dsh-tool-subagent-report-'))
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
const fiber = options.load === false
? undefined
@@ -541,7 +541,7 @@ function userTexts(events: readonly SessionEvent[]): string[] {
}
describe('dsh-tool-subagent-report result independence', () => {
it('does not report a final assistant answer automatically or create Tasks', async () => {
it('does not report a final assistant answer automatically or create Jobs', async () => {
const { ctx, parent, adapter } = await setup()
const { started } = await startChild(ctx, parent)
adapter.release()
@@ -554,6 +554,6 @@ describe('dsh-tool-subagent-report result independence', () => {
// Nothing turns the child's final answer into a report it did not send.
expect(reports(parent)).toEqual([])
expect(userTexts((await ctx.sessionPersistence.load(started.childId)).events)).toEqual(['child task'])
expect(ctx.get('tasks')).toBeUndefined()
expect(ctx.get('jobs')).toBeUndefined()
})
})

View File

@@ -27,7 +27,7 @@
"path": "../subagent"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}

View File

@@ -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/subagent/tool-subagent/README.md
README.md: cc872f3decc4c85f26bd997293c8dc4140eb221c
README.zh.md: bea9b212bf6d551446ba05c8f5b446a0347dd718
README.md: 9d7ed2e364f6a9dff26a1c9006535f898bdaabcc
README.zh.md: 8650ee35588c2615e4d6c016cb672030ee2e8194

View File

@@ -10,7 +10,7 @@ Each plugin instance binds one `provider` to one `toolName`; the model receives
A foreground call passes the execution signal through startup and execution, awaits `run.result`, and always awaits `run.dispose()` before returning. Only `completed` returns the canonical `{ kind: 'foreground', runId, output: JsonValue[] }`, rendered as the same final text; abort, refusal, token limit, and other failures become errored tool results whose message appends the child's preserved partial text (the `SubagentResult.output` selection) after the stop-reason headline, so a truncated answer is never reported as success yet never silently lost. If result collection and disposal both reject, the errored result preserves both diagnostics.
`backgroundMode` selects both the background route and the omitted `run_in_background` default. `one-shot` waits in the foreground by default; an explicit `true` registers a plain parent-owned Task and returns canonical `{ kind: 'background', taskId }`, rendered as `started background subagent task <id>`, even when the provider supports continuable children. Generic task tools own its later status, collection, cancellation, and notices. `continuable` runs in the background when the argument is omitted or `true`; an explicit `false` waits for the result in the foreground. Its background route requires a provider with the `prepareContinuable` capability, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'continuable', subagentId }`, rendered as `started subagent <childId>`. The route resolves at inbox acceptance: the child owns its own turns from there, so this call neither waits for nor collects a result. The child's transcript by that id remains the source of its detailed output, and the optional global `send_message` tool sends it more work. The continuation service delivers one settlement notice whenever the child's Activation ends, containing its outcome and any final assistant message independently of `report`. Starting continuable work does not require `send_message` to be loaded. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and the [background-first delegation Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md).
`backgroundMode` selects both the background route and the omitted `run_in_background` default. `one-shot` waits in the foreground by default; an explicit `true` registers a plain parent-owned Task and returns canonical `{ kind: 'background', jobId }`, rendered as `started background subagent job <id>`, even when the provider supports continuable children. Generic task tools own its later status, collection, cancellation, and notices. `continuable` runs in the background when the argument is omitted or `true`; an explicit `false` waits for the result in the foreground. Its background route requires a provider with the `prepareContinuable` capability, calls `ctx.subagents.startContinuable()`, and returns `{ kind: 'continuable', subagentId }`, rendered as `started subagent <childId>`. The route resolves at inbox acceptance: the child owns its own turns from there, so this call neither waits for nor collects a result. The child's transcript by that id remains the source of its detailed output, and the optional global `send_message` tool sends it more work. The continuation service delivers one settlement notice whenever the child's Activation ends, containing its outcome and any final assistant message independently of `report`. Starting continuable work does not require `send_message` to be loaded. See the [background subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md), the [continuable subagents Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md), and the [background-first delegation Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md).
`toolFilter` changes the child's global tool layer but is not a parent-derived authority ceiling. See the [agent-scope security non-goal](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals).
@@ -29,7 +29,7 @@ A foreground call passes the execution signal through startup and execution, awa
## Concurrency
Foreground and background calls are concurrency-safe: sibling delegations in one assistant message overlap under the loop's rolling pool (`maxParallelToolCalls`), and results still commit in model order. Children work in their own sessions and a run never mutates the parent session; the one-shot background form's one parent-owned write — registering a Task — is a synchronous, commutative insertion that tolerates concurrent dispatch, so overlapping background calls acquire their task ids in dispatch-race order. Coordinating sibling workspace effects belongs to the model, exactly as it already does for background and continuable children. See the [parallel subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) and the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
Foreground and background calls are concurrency-safe: sibling delegations in one assistant message overlap under the loop's rolling pool (`maxParallelToolCalls`), and results still commit in model order. Children work in their own sessions and a run never mutates the parent session; the one-shot background form's one parent-owned write — registering a Task — is a synchronous, commutative insertion that tolerates concurrent dispatch, so overlapping background calls acquire their job ids in dispatch-race order. Coordinating sibling workspace effects belongs to the model, exactly as it already does for background and continuable children. See the [parallel subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) and the [parallel tool-call Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md).
## Model Experience
@@ -37,7 +37,7 @@ Foreground and background calls are concurrency-safe: sibling delegations in one
#### What the model sees
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions. Enabled background mode adds `run_in_background`: continuable mode documents its `true` default, runtime settlement notice, and explicit foreground override, while one-shot mode documents its `false` default and the task id collected with `task_output` or stopped with `task_kill`. While the tool is visible in an assembly's scope, a `tool:<toolName>` system-prompt section tells the model to start independent continuable delegations together, keep working while they run, and choose foreground only when its next action depends on the result; a tool restriction removes both its schema and this guidance.
The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) under this instance's configured name while its provider exists. Provider context inheritance changes the tool and prompt descriptions. Enabled background mode adds `run_in_background`: continuable mode documents its `true` default, runtime settlement notice, and explicit foreground override, while one-shot mode documents its `false` default and the job id collected with `job_output` or stopped with `job_kill`. While the tool is visible in an assembly's scope, a `tool:<toolName>` system-prompt section tells the model to start independent continuable delegations together, keep working while they run, and choose foreground only when its next action depends on the result; a tool restriction removes both its schema and this guidance.
#### Token effect
@@ -65,7 +65,7 @@ Append-only; newly visible content follows the reusable request prefix and does
#### What the model sees
Start returns exactly `started subagent <childId>` in configured continuable mode, or `started background subagent task <id>` in configured one-shot mode. In one-shot mode the generic task surface provides later status, final output, cancellation responses, and notices. In continuable mode this tool returns no result of its own; the child's settlement reaches the parent as a [service-owned notice](../subagent/README.md#settlement-notice), an independently loaded `send_message` tool delivers follow-ups, and the child's transcript by its id is the source of its detailed output.
Start returns exactly `started subagent <childId>` in configured continuable mode, or `started background subagent job <id>` in configured one-shot mode. In one-shot mode the generic task surface provides later status, final output, cancellation responses, and notices. In continuable mode this tool returns no result of its own; the child's settlement reaches the parent as a [service-owned notice](../subagent/README.md#settlement-notice), an independently loaded `send_message` tool delivers follow-ups, and the child's transcript by its id is the source of its detailed output.
#### Token effect

View File

@@ -10,7 +10,7 @@
前台调用会让执行信号贯穿启动和执行,等待 `run.result`,并且在返回前总会等待 `run.dispose()`。只有 `completed` 会返回规范值 `{ kind: 'foreground', runId, output: JsonValue[] }`并渲染为相同的最终文本中止、拒绝、token 上限和其他失败都会变成出错的工具结果,其消息在终止原因标题之后附带子 agent 保留下来的部分文本(即 `SubagentResult.output` 的选取结果)——被截断的回答不会被报告为成功,也绝不会被悄悄丢弃。如果结果收集与 dispose资源释放都 reject出错的结果会保留两项诊断信息。
`backgroundMode` 同时选择后台路由与省略 `run_in_background` 时的默认行为。`one-shot` 默认在前台等待;显式传入 `true` 时,它会注册一个归父级所有的普通 Task并返回规范值 `{ kind: 'background', taskId }`,渲染为 `started background subagent task <id>`,即使提供方支持可继续子 agent 也不例外。通用 Task 工具负责其后续状态、收集、取消和通知。`continuable` 在参数省略或为 `true` 时于后台运行;显式传入 `false` 时则在前台等待结果。其后台路由要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent <childId>`。该路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果。通过该 id 查看其 transcript文本记录仍是其详细输出的来源可选的全局 `send_message` 工具则向其发送更多工作。每当子 agent 的 Activation 结束,继续执行服务都会投递一条结算通知,其中包含结束结果及可能存在的最终 assistant 消息,且这项投递不依赖 `report`。启动可继续工作不要求加载 `send_message`。见[后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[后台优先委派 Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md)。
`backgroundMode` 同时选择后台路由与省略 `run_in_background` 时的默认行为。`one-shot` 默认在前台等待;显式传入 `true` 时,它会注册一个归父级所有的普通 Task并返回规范值 `{ kind: 'background', jobId }`,渲染为 `started background subagent job <id>`,即使提供方支持可继续子 agent 也不例外。通用 Task 工具负责其后续状态、收集、取消和通知。`continuable` 在参数省略或为 `true` 时于后台运行;显式传入 `false` 时则在前台等待结果。其后台路由要求提供方具备 `prepareContinuable` 能力,调用 `ctx.subagents.startContinuable()`,并返回 `{ kind: 'continuable', subagentId }`,渲染为 `started subagent <childId>`。该路由在 inbox 接受时结算:子 agent 自此拥有自己的轮次,因此该调用既不等待也不收集结果。通过该 id 查看其 transcript文本记录仍是其详细输出的来源可选的全局 `send_message` 工具则向其发送更多工作。每当子 agent 的 Activation 结束,继续执行服务都会投递一条结算通知,其中包含结束结果及可能存在的最终 assistant 消息,且这项投递不依赖 `report`。启动可继续工作不要求加载 `send_message`。见[后台 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-08-background-subagent-tasks.md)、[可继续的 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-07-28-continuable-subagent-conversations.md)和[后台优先委派 Agent Note](../../../.agents/notes/implemented/feature/2026-08-11-background-first-continuable-delegation.md)。
`toolFilter` 会改变子 agent 的全局工具层,但不是从父级派生的权限上限。见 [agent 作用域的安全非目标](../../../.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)。
@@ -29,7 +29,7 @@
## 并发
前台调用和后台调用均并发安全:同一条 assistant 消息中的同级委派会在循环的滚动池(`maxParallelToolCalls`)下重叠执行,结果仍按模型顺序提交。子 agent 在各自的会话中工作,一次运行绝不变更父会话;一次性后台形态对父级拥有状态的唯一写入是注册一个 Task——这是一次同步、可交换、能容忍并发分发的插入因此重叠的后台调用按分发竞态顺序获得各自的 task id。协调同级工作区效果由模型负责正如模型已经对后台和可继续子 agent 所承担的那样。见 [并行 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) 和 [并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
前台调用和后台调用均并发安全:同一条 assistant 消息中的同级委派会在循环的滚动池(`maxParallelToolCalls`)下重叠执行,结果仍按模型顺序提交。子 agent 在各自的会话中工作,一次运行绝不变更父会话;一次性后台形态对父级拥有状态的唯一写入是注册一个 Task——这是一次同步、可交换、能容忍并发分发的插入因此重叠的后台调用按分发竞态顺序获得各自的 job id。协调同级工作区效果由模型负责正如模型已经对后台和可继续子 agent 所承担的那样。见 [并行 subagent Agent Note](../../../.agents/notes/implemented/feature/2026-08-09-parallel-subagent-delegations.md) 和 [并行工具调用 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)。
## 模型体验
@@ -37,7 +37,7 @@
#### 模型看到的内容
当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `task_output` 收集或用 `task_kill` 停止的 task id。当工具在本次组装的作用域中可见时一个 `tool:<toolName>` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。
当提供方存在时,以当前实例配置的名称公开已生成的默认 [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent)。提供方是否继承上下文会改变工具描述和提示词描述。启用后台模式会添加 `run_in_background`:可继续模式会记录其默认值为 `true`、运行时结算通知与显式前台覆盖;一次性模式会记录其默认值为 `false`,以及用 `job_output` 收集或用 `job_kill` 停止的 job id。当工具在本次组装的作用域中可见时一个 `tool:<toolName>` 系统提示词 section 会指示模型同时启动相互独立的可继续委派、在它们运行时继续工作,并且仅当下一步动作依赖结果时选择前台;工具限制会同时移除其 schema 和这段指引。
#### Token 影响
@@ -65,7 +65,7 @@
#### 模型看到的内容
在配置的可继续模式下,启动时返回内容恰为 `started subagent <childId>`;在配置的一次性模式下,则返回 `started background subagent task <id>`。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
在配置的可继续模式下,启动时返回内容恰为 `started subagent <childId>`;在配置的一次性模式下,则返回 `started background subagent job <id>`。一次性模式下,通用 Task 接口提供后续状态、最终输出、取消响应和通知。可继续模式下,本工具不返回自己的结果;子 agent 的结算会以[服务负责的通知](../subagent/README.md#settlement-notice)到达父级,独立加载的 `send_message` 工具会投递后续消息,而通过其 id 查看子 agent 的 transcript 即是其详细输出来源。
#### Token 影响

View File

@@ -37,7 +37,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
@@ -53,11 +53,11 @@
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-subagent": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn": "workspace:^",
"@deepseek-ai/dsh-subagent-spawn-in-process": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tasks": "workspace:^",
"@deepseek-ai/dsh-tasks-local": "workspace:^",
"@deepseek-ai/dsh-tool-tasks": "workspace:^",
"@deepseek-ai/dsh-jobs": "workspace:^",
"@deepseek-ai/dsh-jobs-local": "workspace:^",
"@deepseek-ai/dsh-tool-jobs": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}

View File

@@ -16,7 +16,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { JsonValue } from '@deepseek-ai/dsh-session'
import { assertSubagentMaxDepth, settleRun } from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider, SubagentResult, SubagentRun } from '@deepseek-ai/dsh-subagent'
import type { TaskOutcome } from '@deepseek-ai/dsh-tasks'
import type { JobOutcome } from '@deepseek-ai/dsh-jobs'
import type {} from '@deepseek-ai/dsh-system-prompt'
export const name = 'tool-subagent'
@@ -109,7 +109,7 @@ function outputValueText(values: JsonValue[]): string {
}
/** Settle pending startup without rejecting the task producer contract. */
async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Promise<TaskOutcome> {
async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Promise<JobOutcome> {
try {
return await settleRun(await start)
} catch (error: unknown) {
@@ -302,7 +302,7 @@ export function apply(ctx: Context, config: Config): void {
// continuable background path is reachable at all.
? continuable
? ' This tool runs in the background by default, immediately returns a durable subagent id, and keeps the child conversation available for later turns. When that run settles, the runtime sends the parent a notice containing its outcome and any final assistant message; `send_message` starts a later turn in the same child conversation. Set `run_in_background: false` only when your next action depends on receiving the result.'
: ' This call waits for the result by default. Set `run_in_background: true` to return a task id; collect with `task_output` and stop with `task_kill`.'
: ' This call waits for the result by default. Set `run_in_background: true` to return a job id; collect with `job_output` and stop with `job_kill`.'
: ' This call waits for the subagent and returns its result.'),
parameters: {
description: {
@@ -320,7 +320,7 @@ export function apply(ctx: Context, config: Config): void {
type: 'boolean' as const,
description: continuable
? 'Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it.'
: 'Whether to run as a background task and return its id. Defaults to false; collect with task_output or stop with task_kill.',
: 'Whether to run as a background job and return its id. Defaults to false; collect with job_output or stop with job_kill.',
},
} : {},
},
@@ -332,7 +332,7 @@ export function apply(ctx: Context, config: Config): void {
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'background' },
taskId: { type: 'string', required: true },
jobId: { type: 'string', required: true },
},
},
{
@@ -357,7 +357,7 @@ export function apply(ctx: Context, config: Config): void {
render: (_args, value) => [{
type: 'text',
text: value.kind === 'background'
? `started background subagent task ${value.taskId}`
? `started background subagent task ${value.jobId}`
: value.kind === 'continuable'
? `started subagent ${value.subagentId}`
: outputValueText(value.output),
@@ -397,13 +397,13 @@ export function apply(ctx: Context, config: Config): void {
})
return { kind: 'continuable' as const, subagentId: started.childId }
}
const tasks = ctx.get('tasks')
if (tasks === undefined) {
throw new Error('background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks')
const jobs = ctx.get('jobs')
if (jobs === undefined) {
throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs')
}
// One-shot background child: task preflight finishes before the
// One-shot background child: job preflight finishes before the
// starter can spawn, and the task-owned signal covers startup.
const id = tasks.start({
const id = jobs.start({
kind: 'subagent',
label: args.description,
owner: parent,
@@ -419,7 +419,7 @@ export function apply(ctx: Context, config: Config): void {
}
},
})
return { kind: 'background' as const, taskId: id }
return { kind: 'background' as const, jobId: id }
}
const run: SubagentRun = await ctx.subagents.start(config.provider, {

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { type Agent } from '@deepseek-ai/dsh-agent'
import SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import SubagentRuntime, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { SessionId } from '@deepseek-ai/dsh-session'
import * as scripted from './scripted-provider.ts'
@@ -21,7 +21,7 @@ function baseRequest(over: Partial<SubagentStartRequest> = {}): SubagentStartReq
async function mount(config: Partial<scripted.Config> = {}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await scripted.mountScriptedProvider(ctx, { name: 'mock', ...config })
return ctx
}
@@ -89,7 +89,7 @@ describe('scripted subagent provider fixture', () => {
it('unregisters with its owning fixture fiber', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const fiber = await scripted.mountScriptedProvider(ctx, { name: 'mock' })
expect(ctx.subagents.list()).toEqual(['mock'])
await fiber.dispose()

View File

@@ -6,17 +6,17 @@ import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools'
import { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn'
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local'
import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import * as ToolTasks from '@deepseek-ai/dsh-tool-jobs'
import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as mock from './scripted-provider.ts'
import * as tool from '../src/index.ts'
@@ -26,7 +26,7 @@ const testToolSignal = new AbortController().signal
/**
* Drives the REAL plugin body: mounts `dsh-tool-subagent` on a real
* `ToolRegistry` + `SubagentService`, with a package-local scripted child
* `ToolRuntime` + `SubagentRuntime`, with a package-local scripted child
* boundary, and invokes the registered `subagent` tool through
* `ctx.tools.execute`. Everything downstream of the child boundary is the
* shipping code path.
@@ -40,8 +40,8 @@ function fakeAgent(id = 'parent-1'): Agent {
async function setup(toolConfig: tool.Config, mockConfig: Partial<mock.Config> = {}) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig })
await ctx.plugin(tool, toolConfig)
return ctx
@@ -102,7 +102,7 @@ describe('dsh-tool-subagent', () => {
expect(schema).toBeDefined()
const props = (schema!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['description', 'prompt', 'run_in_background'])
expect(schema!.description).toContain('task_output')
expect(schema!.description).toContain('job_output')
})
it('omits run_in_background entirely when the instance disables it (schema and capability never disagree)', async () => {
@@ -110,7 +110,7 @@ describe('dsh-tool-subagent', () => {
const schema = ctx.tools.schemas().find(s => s.name === 'subagent')
const props = (schema!.parameters as { properties?: Record<string, unknown> }).properties ?? {}
expect(Object.keys(props).sort()).toEqual(['description', 'prompt'])
expect(schema!.description).not.toContain('task_output')
expect(schema!.description).not.toContain('job_output')
})
it('refuses a forced run_in_background at execution time when the instance disables it', async () => {
@@ -187,8 +187,8 @@ describe('dsh-tool-subagent', () => {
// names, so a configurable name is what makes this work.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'spawn', reply: 'from spawn' })
await mock.mountScriptedProvider(ctx, { name: 'acp', reply: 'from acp' })
await ctx.plugin(tool, { provider: 'spawn', toolName: 'subagent' })
@@ -208,8 +208,8 @@ describe('dsh-tool-subagent', () => {
// arm must treat an unrecognized terminal reason as a failure, not success.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'weird',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -234,8 +234,8 @@ describe('dsh-tool-subagent', () => {
let seen: { agentOptions?: { model?: string } } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -264,8 +264,8 @@ describe('dsh-tool-subagent', () => {
let seen: { agentOptions?: unknown } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'bare',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -299,8 +299,8 @@ describe('dsh-tool-subagent', () => {
it('registers when the provider appears LATER — no load-order requirement (Loader starts siblings concurrently)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
// Tool first: no provider yet — the tool must be absent, not broken.
// Direct apply (schema bypass): also covers the waiting-note's default
// toolName fallback, which validated config pre-fills.
@@ -316,8 +316,8 @@ describe('dsh-tool-subagent', () => {
it('keeps continuable guidance empty while its provider is absent', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
tool.apply(ctx, {
provider: 'later-continuable',
backgroundMode: 'continuable',
@@ -332,8 +332,8 @@ describe('dsh-tool-subagent', () => {
it('mirrors the provider lifecycle: gone on backend dispose, re-derived wording on re-registration', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
const backend = await mock.mountScriptedProvider(ctx, { name: 'mock' }) // fresh conversation (descriptor: false)
await ctx.plugin(tool, { provider: 'mock' })
expect(ctx.tools.schemas().find(s => s.name === 'subagent')!.description).toContain('does not see this conversation')
@@ -351,8 +351,8 @@ describe('dsh-tool-subagent', () => {
it('the tool PLUGIN fiber owns its lifecycle listeners: disposal unmounts, and a disposed fiber never zombie-mounts', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
// Arm 1: a mounted tool and its prompt section die with the plugin fiber;
// the provider survives.
@@ -387,8 +387,8 @@ describe('dsh-tool-subagent', () => {
it('ignores lifecycle events for OTHER providers', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
await mock.mountScriptedProvider(ctx, { name: 'mock' })
await ctx.plugin(tool, { provider: 'mock' })
// An unrelated provider registering (added-event with another name) and
@@ -423,8 +423,8 @@ describe('dsh-tool-subagent', () => {
const disposed = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -446,8 +446,8 @@ describe('dsh-tool-subagent', () => {
const disposed = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -470,8 +470,8 @@ describe('dsh-tool-subagent', () => {
const disposed = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -498,8 +498,8 @@ describe('dsh-tool-subagent', () => {
it('reports a foreground disposal failure after a completed result', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -525,8 +525,8 @@ describe('dsh-tool-subagent', () => {
const cancelled = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -564,8 +564,8 @@ describe('dsh-tool-subagent', () => {
const sawAborted = vi.fn()
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'spy',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -591,8 +591,8 @@ describe('dsh-tool-subagent', () => {
it('tools depend on the service: no `subagent` tool without ctx.subagents', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
// No SubagentService mounted. The tool injects its three required services so its
await ctx.plugin(ToolRuntime)
// No SubagentRuntime mounted. The tool injects its three required services so its
// apply never runs; the tool is absent rather than half-registered.
let booted = true
try {
@@ -628,8 +628,8 @@ describe('dsh-tool-subagent', () => {
let seen: { persona?: string; toolFilter?: unknown; maxDepth?: number } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture2',
capabilities: { outputSchema: false, depthLimit: true, toolFilter: true, persona: true },
@@ -685,8 +685,8 @@ describe('dsh-tool-subagent', () => {
let seen: { toolFilter?: { readonly allow?: readonly string[]; readonly deny?: readonly string[] } } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture3',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false },
@@ -715,8 +715,8 @@ describe('dsh-tool-subagent', () => {
let seen: { agentOptions?: unknown } | undefined
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture4',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -740,8 +740,8 @@ describe('dsh-tool-subagent', () => {
it('an explicit empty toolFilter fails at plugin load, not at first delegation', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'p',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false },
@@ -772,7 +772,7 @@ describe('dsh-tool-subagent background mode', () => {
async function backgroundSetup(toolConfig: tool.Config, mockConfig: Partial<mock.Config> = {}) {
const ctx = await setup(toolConfig, mockConfig)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks, {})
return ctx
}
@@ -818,21 +818,21 @@ describe('dsh-tool-subagent background mode', () => {
expect(prepareCalls).toBe(0)
})
it('returns a task id immediately and the answer is collected through task_output', async () => {
it('returns a job id immediately and the answer is collected through job_output', async () => {
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } }, { reply: 'background answer' })
const parent = ownerAgent(ctx, 'sess-parent')
const start = await callSubagent(ctx, { description: 'deep research', prompt: 'dig in', run_in_background: true }, { agent: parent })
expect(start.isError).toBe(false)
if (start.isError) throw new Error('expected background subagent success')
expect(start.value).toEqual({ kind: 'background', taskId: 'subagent-1' })
expect(start.value).toEqual({ kind: 'background', jobId: 'subagent-1' })
expect(text(start)).toBe('started background subagent task subagent-1')
const collected = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('collect-1'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(collected)).toBe('background answer\n[status: completed]')
@@ -841,8 +841,8 @@ describe('dsh-tool-subagent background mode', () => {
const again = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('collect-2'),
name: 'task_output',
arguments: { task_id: 'subagent-1' },
name: 'job_output',
arguments: { job_id: 'subagent-1' },
agent: parent,
})
expect(text(again)).toBe('background answer\n[status: completed]')
@@ -852,7 +852,7 @@ describe('dsh-tool-subagent background mode', () => {
const ctx = await setup({ provider: 'mock' })
const result = await callSubagent(ctx, { description: 'd', prompt: 'p', run_in_background: true })
expect(result.isError).toBe(true)
expect(text(result)).toContain('background tasks unavailable: load @deepseek-ai/dsh-tasks')
expect(text(result)).toContain('background jobs unavailable: load @deepseek-ai/dsh-jobs')
})
it('skips background startup when the tool signal is already aborted', async () => {
@@ -891,8 +891,8 @@ describe('dsh-tool-subagent background mode', () => {
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toContain('[status: failed, Error: setup failed]')
@@ -921,21 +921,21 @@ describe('dsh-tool-subagent background mode', () => {
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-kill'),
name: 'task_kill',
arguments: { task_id: 'subagent-1', reason: 'no longer needed' },
name: 'job_kill',
arguments: { job_id: 'subagent-1', reason: 'no longer needed' },
agent: parent,
})
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('pending-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
name: 'job_output',
arguments: { job_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toBe('(no new output)\n[status: killed]')
})
it('forwards task_kill reasons through the run signal (and defaults one when absent)', async () => {
it('forwards job_kill reasons through the run signal (and defaults one when absent)', async () => {
// Use a provider that remains live until its signal is aborted.
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })
const parent = ownerAgent(ctx, 'sess-parent')
@@ -969,14 +969,14 @@ describe('dsh-tool-subagent background mode', () => {
expect(text(startOne)).toBe('started background subagent task subagent-1')
expect(text(startTwo)).toBe('started background subagent task subagent-2')
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'task_kill', arguments: { task_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'task_kill', arguments: { task_id: 'subagent-2' }, agent: parent })
expect(text(withReason)).toBe('requested cancellation of task subagent-1')
expect(text(withoutReason)).toBe('requested cancellation of task subagent-2')
const withReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k1'), name: 'job_kill', arguments: { job_id: 'subagent-1', reason: 'superseded' }, agent: parent })
const withoutReason = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('k2'), name: 'job_kill', arguments: { job_id: 'subagent-2' }, agent: parent })
expect(text(withReason)).toBe('requested cancellation of job subagent-1')
expect(text(withoutReason)).toBe('requested cancellation of job subagent-2')
expect(cancels).toEqual(['superseded', 'background subagent task killed'])
// The aborted children settle as killed tasks.
const killed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('w1'), name: 'task_output', arguments: { task_id: 'subagent-1', wait: true }, agent: parent })
const killed = await ctx.tools.execute({ signal: testToolSignal, callId: CallId('w1'), name: 'job_output', arguments: { job_id: 'subagent-1', wait: true }, agent: parent })
expect(text(killed)).toBe('(no new output)\n[status: killed]')
})
@@ -996,9 +996,9 @@ describe('dsh-tool-subagent continuable background mode', () => {
roots.push(root)
await ctx.plugin(JsonlSessionPersistence, { root })
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(SubagentSpawn, { providerName: 'spawn' })
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalJobRegistry)
await ctx.plugin(ToolTasks, {})
await ctx.plugin(tool, { provider: 'spawn', backgroundMode: 'continuable' })
ctx.llm.registerAdapter(['mock'], new MockAdapter([
@@ -1022,8 +1022,8 @@ describe('dsh-tool-subagent continuable background mode', () => {
const { ctx, parent } = await continuableSetup()
const schema = ctx.tools.schemas().find(s => s.name === 'subagent')!
// Continuable delegation has no Task, so the schema promises no collection.
expect(schema.description).not.toContain('task_output')
expect(schema.description).not.toContain('task_kill')
expect(schema.description).not.toContain('job_output')
expect(schema.description).not.toContain('job_kill')
expect(schema.description).toContain('send_message')
expect(schema.description).toContain('runs in the background by default')
expect(schema.description).not.toContain('never poll or wait on it')
@@ -1046,7 +1046,7 @@ describe('dsh-tool-subagent continuable background mode', () => {
expect(match).not.toBeNull()
const [, childId] = match!
// No Task was created for the continuable child.
expect(ctx.tasks.list(parent)).toEqual([])
expect(ctx.jobs.list(parent)).toEqual([])
await vi.waitFor(() => {
expect(ctx.agents.get(SessionId(childId!))).toBeUndefined()
@@ -1077,7 +1077,7 @@ describe('dsh-tool-subagent continuable background mode', () => {
if (result.isError) throw new Error('expected foreground subagent success')
expect(result.value).toMatchObject({ kind: 'foreground' })
expect(text(result)).toBe('continuable answer')
expect(ctx.tasks.list(parent)).toEqual([])
expect(ctx.jobs.list(parent)).toEqual([])
})
it('isolates a cancelled continuable preparation from a concurrent sibling', async () => {
@@ -1147,10 +1147,10 @@ describe('dsh-tool-subagent continuable background mode', () => {
describe('background preflight failure (no orphaned child, by construction)', () => {
it('never starts the child when tasks.start preflight throws', async () => {
// With no task controller, preflight fails before the provider can spawn.
// With no job controller, preflight fails before the provider can spawn.
const ctx = await setup({ provider: 'mock' })
await ctx.plugin(AgentRegistry)
await ctx.plugin(LocalTaskService)
await ctx.plugin(LocalJobRegistry)
const scopeFiber = ctx.plugin(() => {})
const id = SessionId('sess-p')
const parent = {
@@ -1187,7 +1187,7 @@ describe('background preflight failure (no orphaned child, by construction)', ()
agent: parent,
})
expect(result.isError).toBe(true)
expect(text(result)).toContain('no task controller serves this agent')
expect(text(result)).toContain('no job controller serves this agent')
// Declare-then-execute: the failed preflight means no child ever existed.
expect(starts).toBe(0)
})
@@ -1199,8 +1199,8 @@ describe('depth budget configuration', () => {
const requests: SubagentStartRequest[] = []
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'capture',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
@@ -1237,8 +1237,8 @@ describe('depth budget configuration', () => {
it('rejects a numeric maxDepth on a provider without the depthLimit capability at mount', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'no-depth',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
@@ -1253,8 +1253,8 @@ describe('depth budget configuration', () => {
const requests: SubagentStartRequest[] = []
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(SubagentService)
await ctx.plugin(ToolRuntime)
await ctx.plugin(SubagentRuntime)
ctx.subagents.registerProvider({
name: 'external',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },

View File

@@ -33,10 +33,10 @@
"path": "../subagent"
},
{
"path": "../../tasks/tasks"
"path": "../../jobs/jobs"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}