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/workflow/README.md
README.md: 7b19cfd37f9b6870825d5e0761aed92908206b3a
README.zh.md: 33b3f26780da1bf4efcd78242b2cfb212cef4c22
README.md: 156e9f0d84ced4eab8c25da91d29d239d18b0629
README.zh.md: 24687f81d612c23786150c5b5a70bce352954663

View File

@@ -6,8 +6,8 @@ This family runs model-authored orchestration workflows over subagents and expos
| Package | Role | ctx key |
|---|---|---|
| [`workflow/`](workflow/README.md) | Defines workflow execution and lifecycle events | `ctx.workflows` |
| [`workflow-workerthread/`](workflow-workerthread/README.md) | Runs workflow scripts in worker threads | registers on `ctx.workflows` |
| [`workflow/`](workflow/README.md) | Defines workflow execution and lifecycle events | `ctx.workflowEngine` |
| [`workflow-worker-thread/`](workflow-worker-thread/README.md) | Runs workflow scripts in worker threads | registers on `ctx.workflowEngine` |
| [`tool-workflow/`](tool-workflow/README.md) | Exposes general workflow execution to the model | registers on `ctx.tools` |
| [`tool-ralph/`](tool-ralph/README.md) | Exposes the fixed fresh-agent Ralph workflow | registers on `ctx.tools` |

View File

@@ -6,8 +6,8 @@
| 包 | 职责 | ctx 键 |
|---|---|---|
| [`workflow/`](workflow/README.md) | 定义工作流执行和生命周期事件 | `ctx.workflows` |
| [`workflow-workerthread/`](workflow-workerthread/README.md) | 在线程中运行工作流脚本 | 注册到 `ctx.workflows` |
| [`workflow/`](workflow/README.md) | 定义工作流执行和生命周期事件 | `ctx.workflowEngine` |
| [`workflow-worker-thread/`](workflow-worker-thread/README.md) | 在线程中运行工作流脚本 | 注册到 `ctx.workflowEngine` |
| [`tool-workflow/`](tool-workflow/README.md) | 向模型公开通用工作流执行 | 注册到 `ctx.tools` |
| [`tool-ralph/`](tool-ralph/README.md) | 公开使用全新 agent智能体的固定 Ralph 工作流 | 注册到 `ctx.tools` |

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/workflow/tool-ralph/README.md
README.md: d5389a08cf4aa63d6f00939f5dafab421816bf4a
README.zh.md: d01ae6792b808f330d65da2589a4394d1923110b
README.md: ae9d7c80763f751860bc2073950dde331fdd5a8e
README.zh.md: 692ebb693b54ade2c2fbde18f818ced10e9d5863

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The model-facing `ralph` tool runs a fixed foreground workflow that gives one immutable objective to a sequence of fresh child agents. It demonstrates a specialized orchestration policy as an ordinary plugin over [`ctx.workflows`](../workflow/README.md) and [`ctx.subagents`](../../subagent/subagent/README.md): no Ralph mode or fresh-agent loop is added to `agent-loop`, and the same-session [goal domain](../../goal/goal/README.md) remains independent. The [Ralph Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) owns the policy and deferred work.
The model-facing `ralph` tool runs a fixed foreground workflow that gives one immutable objective to a sequence of fresh child agents. It demonstrates a specialized orchestration policy as an ordinary plugin over [`ctx.workflowEngine`](../workflow/README.md) and [`ctx.subagents`](../../subagent/subagent/README.md): no Ralph mode or fresh-agent loop is added to `agent-loop`, and the same-session [goal domain](../../goal/goal/README.md) remains independent. The [Ralph Agent Note](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md) owns the policy and deferred work.
## Contract
@@ -44,7 +44,7 @@ Every parent request in this plugin's registration scope receives the fixed rout
##### Ralph guidance
```markdown
Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflowEngine for bounded delegation and fan-out.
```
#### Token effect
@@ -86,7 +86,7 @@ Each fresh child has an independent request cache. The parent result appends aft
## Known Limitations and Deferred Work
- **Completion is worker self-declaration** — there is no independent evaluator or verifier deciding whether the objective is actually complete; evaluator policy and evaluator-driven continuation are deferred.
- **Foreground only** — there is no task id, background collection, process-resume checkpoint, scheduler, or wall-clock start policy.
- **Foreground only** — there is no job id, background collection, process-resume checkpoint, scheduler, or wall-clock start policy.
- **The workspace is the only cross-round long-term memory** — one bounded report is the explicit handoff, and uncommitted conversational reasoning disappears with each child.
- **One round is one fresh child** — there is no within-round fan-out, model/provider switching, fork context, or model-call-selected provider.
- **Ordinary child failure is terminal for the run** — the fixed script reports the failed round and last successful handoff but does not retry; fatal workflow infrastructure failures can end before that state is returned.

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
面向模型的 `ralph` 工具运行固定的前台工作流,把一个不可变目标依次交给多个全新子 agent智能体。它展示如何把专用编排策略实现为基于 [`ctx.workflows`](../workflow/README.md) 和 [`ctx.subagents`](../../subagent/subagent/README.md) 的普通插件:不会向 `agent-loop` 添加 Ralph 模式或全新 agent loop智能体循环同会话的[目标领域](../../goal/goal/README.md)也保持独立。策略和暂缓事项由 [Ralph Agent Noteagent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md)负责。
面向模型的 `ralph` 工具运行固定的前台工作流,把一个不可变目标依次交给多个全新子 agent智能体。它展示如何把专用编排策略实现为基于 [`ctx.workflowEngine`](../workflow/README.md) 和 [`ctx.subagents`](../../subagent/subagent/README.md) 的普通插件:不会向 `agent-loop` 添加 Ralph 模式或全新 agent loop智能体循环同会话的[目标领域](../../goal/goal/README.md)也保持独立。策略和暂缓事项由 [Ralph Agent Noteagent 决策记录)](../../../.agents/notes/implemented/feature/2026-07-19-fresh-agent-ralph-workflow-tool.md)负责。
## 契约
@@ -44,7 +44,7 @@
##### Ralph 指导
```markdown
Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflows for bounded delegation and fan-out.
Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop or fresh-agent iterative execution. Each Ralph round starts a fresh child with no conversation seed and uses the shared workspace as durable memory. Completion and blockers are worker reports, not independent evaluation. Use same-session goal tools for ordinary long-running objectives, and plain subagents or workflowEngine for bounded delegation and fan-out.
```
#### Token 影响
@@ -86,7 +86,7 @@ Use the ralph tool ONLY when the direct human explicitly asks for a Ralph loop o
## 已知限制与暂缓事项
- **完成由 worker 自行声明**:没有独立的评估器或验证器判断目标是否实际完成;评估器策略及评估器驱动的延续均暂缓处理。
- **仅支持前台**:没有 task id、后台收集、进程恢复检查点、调度器或基于挂钟时间的启动策略。
- **仅支持前台**:没有 job id、后台收集、进程恢复检查点、调度器或基于挂钟时间的启动策略。
- **工作区是唯一的跨 Round 长期记忆**:一份有界报告作为显式交接内容,每个子 agent 结束后,未提交的对话推理都会消失。
- **一个 Round 对应一个全新子 agent**Round 内没有扇出、模型/提供方切换、fork 上下文或由模型调用选择的提供方。
- **普通子 agent 失败会终止运行**:固定脚本报告失败的 Round 和上一次成功交接,但不会重试;致命的工作流基础设施失败可能在该状态返回前结束。

View File

@@ -53,12 +53,12 @@
"@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/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-workflow": "workspace:^",
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"@deepseek-ai/dsh-workflow-worker-thread": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -17,7 +17,7 @@ import type { WorkflowResult, WorkflowRun } from '@deepseek-ai/dsh-workflow'
import type {} from '@deepseek-ai/dsh-system-prompt'
export const name = 'tool-ralph'
export const inject = ['tools', 'workflows', 'subagents', 'systemPrompt']
export const inject = ['tools', 'workflowEngine', 'subagents', 'systemPrompt']
/** Deployment policy for the fixed Ralph workflow. */
export interface Config {
@@ -444,7 +444,7 @@ export function apply(ctx: Context, config: Config): void {
const maxRounds = resolveMaxRounds(args.maxRounds, resolved.maxRounds)
void requireFreshProvider(ctx, resolved.subagentProvider)
const run: WorkflowRun = ctx.workflows.start({
const run: WorkflowRun = ctx.workflowEngine.start({
script: RALPH_SCRIPT,
meta: RALPH_META,
args: { objective, maxRounds, maxHandoffChars: resolved.maxHandoffChars },

View File

@@ -5,10 +5,10 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentService from '@deepseek-ai/dsh-subagent'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
import * as spawn from '@deepseek-ai/dsh-subagent-spawn'
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver'
import * as spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import WorkerThreadWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import * as toolRalph from '../src/index.ts'
@@ -21,9 +21,9 @@ async function mountRalph(script: MockScript, config: toolRalph.Config) {
const adapter = new MockAdapter(script)
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(spawn, { providerName: 'spawn' })
await ctx.plugin(WorkerWorkflowEngine, {})
await ctx.plugin(WorkerThreadWorkflowEngine, {})
await ctx.plugin(toolRalph, config)
ctx.llm.registerAdapter(['mock'], adapter)
const parentHandle = await ctx.agents.create({
@@ -58,9 +58,9 @@ describe('dsh-tool-ralph over the real spawn and worker-thread stack', () => {
])
await mountAgentLoopTestDependencies(ctx)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
await ctx.plugin(spawn, { providerName: 'spawn' })
await ctx.plugin(WorkerWorkflowEngine, {})
await ctx.plugin(WorkerThreadWorkflowEngine, {})
await ctx.plugin(toolRalph, { maxRounds: 2 })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -4,18 +4,18 @@ import Loader from '@deepseek-ai/cordis-plugin-loader'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentCapabilities, SubagentProvider, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
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 type { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import { WorkflowRunId, WorkflowService } from '@deepseek-ai/dsh-workflow'
import { WorkflowRunId, WorkflowEngine } from '@deepseek-ai/dsh-workflow'
import type { WorkflowResult, WorkflowRun, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow'
import * as toolRalph from '../src/index.ts'
const testToolSignal = new AbortController().signal
class StubEngine extends WorkflowService {
class StubEngine extends WorkflowEngine {
requests: WorkflowStartRequest[] = []
cancels: string[] = []
disposed = 0
@@ -77,8 +77,8 @@ interface SetupOptions {
async function setup(options?: SetupOptions) {
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 provider = options?.provider === false ? undefined : options?.provider ?? new StubProvider()
if (provider !== undefined) ctx.subagents.registerProvider(provider)
await ctx.plugin(StubEngine)
@@ -89,7 +89,7 @@ async function setup(options?: SetupOptions) {
if (options?.config?.maxResultChars !== undefined) config.maxResultChars = options.config.maxResultChars
const fiber = await ctx.plugin(toolRalph, config)
const parent = { id: SessionId('caller'), options: {} } as unknown as Agent
return { ctx, engine: ctx.workflows as StubEngine, parent, fiber }
return { ctx, engine: ctx.workflowEngine as StubEngine, parent, fiber }
}
function execute(
@@ -396,7 +396,7 @@ describe('dsh-tool-ralph', () => {
it('has the namespace-plugin export shape', () => {
expect('default' in toolRalph).toBe(false)
expect(toolRalph.name).toBe('tool-ralph')
expect(toolRalph.inject).toEqual(['tools', 'workflows', 'subagents', 'systemPrompt'])
expect(toolRalph.inject).toEqual(['tools', 'workflowEngine', 'subagents', 'systemPrompt'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(toolRalph) as Record<string, unknown>
expect(unwrapped).toBe(toolRalph)

View File

@@ -36,7 +36,7 @@
"path": "../workflow"
},
{
"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/workflow/tool-workflow/README.md
README.md: 349184a6e56e64cc3775990b9031ffecba7fcbb0
README.zh.md: 22e7f273c76616722f4f97f9f6e0be4c90eaacdd
README.md: 918fe5b9aff74b3b959107ec1eebfd7966afe6d1
README.zh.md: f7e62560a1b581cf64d78dc379b821d17f29b3aa

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The model-facing **`workflow` tool**: run a JavaScript orchestration script that fans out subagents, and return the script's final value. This package owns the model-facing schema and run lifecycle over [`ctx.workflows`](../workflow/README.md); script parsing, execution, caps, and cancellation live behind the seam, while the consumer retains ownership of the parent-facing schema and result envelope.
The model-facing **`workflow` tool**: run a JavaScript orchestration script that fans out subagents, and return the script's final value. This package owns the model-facing schema and run lifecycle over [`ctx.workflowEngine`](../workflow/README.md); script parsing, execution, caps, and cancellation live behind the seam, while the consumer retains ownership of the parent-facing schema and result envelope.
## What the model sees

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
面向模型的 **`workflow` 工具**:运行一段扇出 subagent 的 JavaScript 编排脚本,并返回脚本的最终值。本包负责基于 [`ctx.workflows`](../workflow/README.md) 定义面向模型的 schema 和运行生命周期;脚本解析、执行、上限与取消位于 seam 之后,消费方仍负责面向父级的 schema 和结果包络。
面向模型的 **`workflow` 工具**:运行一段扇出 subagent 的 JavaScript 编排脚本,并返回脚本的最终值。本包负责基于 [`ctx.workflowEngine`](../workflow/README.md) 定义面向模型的 schema 和运行生命周期;脚本解析、执行、上限与取消位于 seam 之后,消费方仍负责面向父级的 schema 和结果包络。
## 模型看到的内容

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-tool-workflow",
"description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflows",
"description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"
@@ -58,7 +58,7 @@
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-workflow": "workspace:^",
"@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
"@deepseek-ai/dsh-workflow-worker-thread": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -1,7 +1,7 @@
/**
* The model-facing `workflow` tool: run a JavaScript orchestration script that fans out
* subagents, and return the script's final value. It owns the model-facing schema and run lifecycle; script
* parsing, execution, caps, and cancellation live behind `ctx.workflows`
* parsing, execution, caps, and cancellation live behind `ctx.workflowEngine`
* (`@deepseek-ai/dsh-workflow`), so a hardened engine swaps in without touching what the model
* sees. Execution awaits `run.result` and always disposes the run; non-completed reasons become tool
* errors, and background collection remains deferred. Presentation is an args-only generic card
@@ -27,7 +27,7 @@ import type {
import type {} from '@deepseek-ai/dsh-system-prompt'
export const name = 'tool-workflow'
export const inject = ['tools', 'workflows', 'systemPrompt']
export const inject = ['tools', 'workflowEngine', 'systemPrompt']
/** Config: the model-facing tool name plus result rendering caps. */
export interface Config {
@@ -281,7 +281,7 @@ export function apply(ctx: Context, config: Config): void {
// Meta/body validation failures (META_INVALID/SCRIPT_PARSE) throw
// synchronously here and become isError results via the registry — the
// model sees the violation list and can correct the call.
const run = ctx.workflows.start({
const run = ctx.workflowEngine.start({
script: args.script,
meta: args.meta,
...args.args !== undefined ? { args: args.args } : {},

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import InvariantService, { InvariantError } from '@deepseek-ai/dsh-invariants'
import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
import { WorkflowRunId, type WorkflowRunId as WorkflowRunIdType } from '@deepseek-ai/dsh-workflow/types'
import * as ToolWorkflowInvariant from '../src/invariant.ts'
@@ -9,7 +9,7 @@ import type {} from '../src/types.ts'
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(InvariantRegistry, { enabled: true })
await ctx.plugin(ToolWorkflowInvariant)
return ctx
}
@@ -182,7 +182,7 @@ describe('durable workflow-record invariants', () => {
valid.append('tool-workflow/agent-start', {
runId: WorkflowRunId('valid'), seq: 1, label: 'open', childId: SessionId('child'),
})
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(InvariantRegistry, { enabled: true })
await expect(ctx.plugin(ToolWorkflowInvariant)).resolves.toBeDefined()
const brokenCtx = new Context()
@@ -193,7 +193,7 @@ describe('durable workflow-record invariants', () => {
broken.append('tool-workflow/agent-start', {
runId: WorkflowRunId('broken'), seq: 1, label: 'late', childId: SessionId('late'),
})
await brokenCtx.plugin(InvariantService, { enabled: true })
await brokenCtx.plugin(InvariantRegistry, { enabled: true })
await expect(brokenCtx.plugin(ToolWorkflowInvariant)).rejects.toThrow(/appears after/)
})
})

View File

@@ -2,24 +2,24 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
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 type { ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { WorkflowRunId, WorkflowService } from '@deepseek-ai/dsh-workflow'
import { WorkflowRunId, WorkflowEngine } from '@deepseek-ai/dsh-workflow'
import type {
WorkflowAgentEndInfo, WorkflowAgentInfo, WorkflowResult, WorkflowRun,
WorkflowRunId as WorkflowRunIdType, WorkflowStartRequest,
} from '@deepseek-ai/dsh-workflow'
import { CallId } from '@deepseek-ai/dsh-llm'
import SubagentService from '@deepseek-ai/dsh-subagent'
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import WorkerThreadWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread'
import * as toolWorkflow from '../src/index.ts'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
const testToolSignal = new AbortController().signal
/** A controllable engine standing in behind ctx.workflows (the tool's only seam). */
class StubEngine extends WorkflowService {
/** A controllable engine standing in behind ctx.workflowEngine (the tool's only seam). */
class StubEngine extends WorkflowEngine {
requests: WorkflowStartRequest[] = []
cancels: string[] = []
disposed = 0
@@ -77,10 +77,10 @@ class StubEngine extends WorkflowService {
async function setup(config?: { toolName?: string; maxResultChars?: number }) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(StubEngine)
await ctx.plugin(toolWorkflow, config ?? {})
const engine = ctx.workflows as StubEngine
const engine = ctx.workflowEngine as StubEngine
const session = Session.create(SessionId('caller'))
const parent = { id: session.id, options: {}, session } as unknown as Agent
return { ctx, engine, parent, session }
@@ -368,7 +368,7 @@ describe('dsh-tool-workflow', () => {
it('registers under a configured toolName and unregisters on fiber dispose (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(ToolRuntime)
await ctx.plugin(StubEngine)
const fiber = await ctx.plugin(toolWorkflow, { toolName: 'orchestrate' })
expect(ctx.tools.get('orchestrate')).toBeDefined()
@@ -406,7 +406,7 @@ describe('dsh-tool-workflow', () => {
it('has the namespace-plugin export shape (no stray default)', () => {
expect('default' in toolWorkflow).toBe(false)
expect(toolWorkflow.name).toBe('tool-workflow')
expect(toolWorkflow.inject).toEqual(['tools', 'workflows', 'systemPrompt'])
expect(toolWorkflow.inject).toEqual(['tools', 'workflowEngine', 'systemPrompt'])
const loader = Object.create(Loader.prototype) as Loader
const unwrapped = loader.unwrapExports(toolWorkflow) as Record<string, unknown>
expect(unwrapped).toBe(toolWorkflow)
@@ -419,15 +419,15 @@ describe('dsh-tool-workflow', () => {
// parked on an unowned promise. Exercise that guarantee through the real registry and worker.
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: 'spawn',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
inheritsParentContext: false,
start: () => Promise.reject(new Error('the parked-script fixture must not start a child')),
})
await ctx.plugin(WorkerWorkflowEngine, { disposeGraceMs: 30 })
await ctx.plugin(WorkerThreadWorkflowEngine, { disposeGraceMs: 30 })
await ctx.plugin(toolWorkflow, {})
const session = Session.create(SessionId('caller'))
const parent = { id: session.id, options: {}, session } as unknown as Agent

View File

@@ -36,7 +36,7 @@
"path": "../workflow"
},
{
"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/workflow/workflow-workerthread/README.md
README.md: 9da420bdd67ac5b0a4bfffa312c6fe7b2dabf8f5
README.zh.md: dbed654508bf38bd369247c411d3e8d3a1a749a5
# pnpm run verify-translation-pairing --write packages/workflow/workflow-worker-thread/README.md
README.md: ba32c9dab4e870dd52c8f8acba3cfa627fa78000
README.zh.md: 99107aec40a45c8460f10082f2b75b908e3e8692

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-workflow-workerthread
# @deepseek-ai/dsh-workflow-worker-thread
English | [中文](README.zh.md)
This package implements `WorkflowService` with one Node worker thread per run. The worker executes the orchestration script; child agents remain on the host and are reached through `ctx.subagents` over a typed host/worker protocol.
This package implements `WorkflowEngine` with one Node worker thread per run. The worker executes the orchestration script; child agents remain on the host and are reached through `ctx.subagents` over a typed host/worker protocol.
The package root exports the default engine plugin and its `Config`; the worker protocol, runtime, and session modules stay private to the implementation. The operational `./worker` entry remains the engine's spawn target.
@@ -41,7 +41,7 @@ Unknown options, malformed arguments, unsupported schemas, tripped caps, provide
For each `agent()` call:
1. The worker sends `child-start` with a plain-data prompt and options.
2. The host calls the start request's provider override, or otherwise the configured provider, through async `SubagentService.start`, passing the workflow's parent and one canonical per-run abort signal. Provider choice applies to every child in that run and is not visible to the script.
2. The host calls the start request's provider override, or otherwise the configured provider, through async `SubagentRuntime.start`, passing the workflow's parent and one canonical per-run abort signal. Provider choice applies to every child in that run and is not visible to the script.
3. If start rejects, the host sends `child-start-error`; provider startup has already reached quiescence and no child lifecycle event is emitted.
4. If start fulfills while the workflow still admits work, the host records the run, observes `result`, then sends `child-started`. Even an already-settled result is forwarded afterward, preserving start-before-result order.
5. The worker emits paired `workflow/agent-start` and `workflow/agent-end` narration and requests child disposal after collection.

View File

@@ -1,8 +1,8 @@
# @deepseek-ai/dsh-workflow-workerthread
# @deepseek-ai/dsh-workflow-worker-thread
[English](README.md) | 中文
本包为 `WorkflowService` 提供实现,每次运行使用一个 Node worker thread。worker 执行编排脚本;子 agent智能体留在宿主上脚本通过带类型的宿主worker 协议经由 `ctx.subagents` 访问它们。
本包为 `WorkflowEngine` 提供实现,每次运行使用一个 Node worker thread。worker 执行编排脚本;子 agent智能体留在宿主上脚本通过带类型的宿主worker 协议经由 `ctx.subagents` 访问它们。
包根目录默认导出引擎插件及其 `Config`worker 协议、运行时和会话模块均为实现私有。操作入口 `./worker` 仍是引擎的 spawn 目标。
@@ -41,7 +41,7 @@ worker 仍提供实用的隔离:
对于每次 `agent()` 调用:
1. worker 发送 `child-start`,其中包含普通数据提示词和选项。
2. 宿主通过异步 `SubagentService.start` 调用启动请求中指定的提供方,否则调用已配置的提供方;调用会传入工作流父级和该次运行共用的唯一中止信号。提供方选择应用于该次运行的每个子 agent对脚本不可见。
2. 宿主通过异步 `SubagentRuntime.start` 调用启动请求中指定的提供方,否则调用已配置的提供方;调用会传入工作流父级和该次运行共用的唯一中止信号。提供方选择应用于该次运行的每个子 agent对脚本不可见。
3. 如果启动被拒绝,宿主会发送 `child-start-error`;提供方启动已经完全停稳,不会发出子 agent 生命周期事件。
4. 如果启动兑现时工作流仍接纳工作,宿主会记录该运行、观察 `result`,然后发送 `child-started`。即使结果已经结算,也只会随后转发,以保持先启动、后结果的顺序。
5. worker 发出成对的 `workflow/agent-start``workflow/agent-end` 叙述,并在收集后请求 dispose 子 agent。

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-workflow-workerthread",
"name": "@deepseek-ai/dsh-workflow-worker-thread",
"description": "worker-thread workflow engine: executes model-written orchestration scripts off the host event loop, bridging agent() calls back to ctx.subagents",
"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/workflow/workflow-workerthread"
"directory": "packages/workflow/workflow-worker-thread"
},
"type": "module",
"main": "lib/index.js",
@@ -59,7 +59,7 @@
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "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-tools": "workspace:^",
"@deepseek-ai/dsh-workflow": "workspace:^",

View File

@@ -3,7 +3,7 @@
* cancellation-grace expiry owns settlement and closes message admission.
* Pending starts share one abort signal; published children share idempotent
* cleanup, and quiescence waits for both while synthesizing any missing end events.
* @module @deepseek-ai/dsh-workflow-workerthread/host
* @module @deepseek-ai/dsh-workflow-worker-thread/host
*/
import { Worker } from 'node:worker_threads'
@@ -13,7 +13,7 @@ import type { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import type SubagentService from '@deepseek-ai/dsh-subagent'
import type SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentRun } from '@deepseek-ai/dsh-subagent'
import type { WorkflowAgentEndInfo, WorkflowAgentInfo, WorkflowMeta, WorkflowResult, WorkflowRun, WorkflowRunId } from '@deepseek-ai/dsh-workflow'
import { renderThrown } from './realm.ts'
@@ -67,7 +67,7 @@ function resolveWorkerSpawn(init: WorkerInit): { entry: string | URL; options: W
* `start()` directly. Owns the Worker, the child registry, and the result
* settlement; `result` never rejects. `meta` is trusted same-process data
* borrowed as immutable by the handle and lifecycle events. The holder-bound
* SubagentService handle is captured before the
* SubagentRuntime handle is captured before the
* engine returns this run, so unloading the engine removes only the ability to
* start another workflow; this run can still start and clean up its children.
*/
@@ -103,7 +103,7 @@ export class WorkerRun implements WorkflowRun {
constructor(
private readonly ctx: Context,
private readonly subagents: SubagentService,
private readonly subagents: SubagentRuntime,
readonly id: WorkflowRunId,
readonly meta: WorkflowMeta,
private readonly parent: Agent,
@@ -233,7 +233,7 @@ export class WorkerRun implements WorkflowRun {
// data, so serialization cannot fail); there is nothing left to
// deliver to — log and move on.
/* v8 ignore next -- postMessage teardown race (a throw between exit and its event): not constructible in-process */
this.ctx.logger.warn(`workflow-workerthread: postMessage failed: ${renderThrown(error)}`)
this.ctx.logger.warn(`workflow-worker-thread: postMessage failed: ${renderThrown(error)}`)
}
}
@@ -349,7 +349,7 @@ export class WorkerRun implements WorkflowRun {
try {
await run.dispose()
} catch (error: unknown) {
this.ctx.logger.warn(`workflow-workerthread: refused child dispose failed: ${renderThrown(error)}`)
this.ctx.logger.warn(`workflow-worker-thread: refused child dispose failed: ${renderThrown(error)}`)
}
return
}
@@ -412,7 +412,7 @@ export class WorkerRun implements WorkflowRun {
record.disposal = Promise.resolve()
.then(() => record.run.dispose())
.catch((error: unknown) => {
this.ctx.logger.warn(`workflow-workerthread: child dispose failed: ${renderThrown(error)}`)
this.ctx.logger.warn(`workflow-worker-thread: child dispose failed: ${renderThrown(error)}`)
})
.then(() => { this.finishChild(callId) })
return record.disposal

View File

@@ -3,7 +3,7 @@
* an escapable vm context on a fresh worker and bridges `agent()` calls to host
* subagents. The thread prevents synchronous script work from blocking the host
* and permits forced termination, but it is containment rather than a security boundary.
* @module @deepseek-ai/dsh-workflow-workerthread
* @module @deepseek-ai/dsh-workflow-worker-thread
*/
import { randomUUID } from 'node:crypto'
@@ -11,7 +11,7 @@ import { availableParallelism } from 'node:os'
import * as vm from 'node:vm'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import WorkflowService, { WorkflowError, WorkflowRunId } from '@deepseek-ai/dsh-workflow'
import WorkflowEngine, { WorkflowError, WorkflowRunId } from '@deepseek-ai/dsh-workflow'
import type { WorkflowRun, WorkflowRunInfo, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow'
import { WorkerRun } from './host.ts'
import { validateMeta } from './meta.ts'
@@ -109,7 +109,7 @@ function resolveMaxTotalAgents(requested: number | undefined, ceiling: number):
* `result` never rejects; the `workflow/*` events fire around the run per
* the seam contract.
*/
class WorkerWorkflowEngine extends WorkflowService {
class WorkerThreadWorkflowEngine extends WorkflowEngine {
static inject = ['subagents']
static Config: z<Config> = z.object({
@@ -163,8 +163,8 @@ class WorkerWorkflowEngine extends WorkflowService {
}
// Capture the dependency while this service call is still traced through
// the start() holder. Cordis strips the engine-provider shadow when it
// returns the SubagentService handle, so an already-returned run can keep
// starting children after an engine HMR unload removes ctx.workflows.
// returns the SubagentRuntime handle, so an already-returned run can keep
// starting children after an engine HMR unload removes ctx.workflowEngine.
// Re-resolving `this.ctx.subagents` later from WorkerRun would instead walk
// the now-inactive engine fiber and break the seam's holder-owned lifetime.
const runCtx = this.ctx
@@ -202,4 +202,4 @@ class WorkerWorkflowEngine extends WorkflowService {
}
}
export default WorkerWorkflowEngine
export default WorkerThreadWorkflowEngine

View File

@@ -1,16 +1,16 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-workflow-workerthread`.
* @module @deepseek-ai/dsh-workflow-workerthread/invariant
* Package-owned invariant companion for `@deepseek-ai/dsh-workflow-worker-thread`.
* @module @deepseek-ai/dsh-workflow-worker-thread/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-workflow-workerthread'
const PACKAGE_NAME = '@deepseek-ai/dsh-workflow-worker-thread'
/** Cordis companion plugin name. */
export const name = 'workflow-workerthread-invariant'
export const name = 'workflow-worker-thread-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']

View File

@@ -3,7 +3,7 @@
* contract and rejects every violation by name. Meta arrives as schema-checked
* JSON data, never evaluated script text; evaluating it on the host could run getters outside the
* worker timeout that exists to isolate model-written code.
* @module @deepseek-ai/dsh-workflow-workerthread/meta
* @module @deepseek-ai/dsh-workflow-worker-thread/meta
*/
import { WorkflowError } from '@deepseek-ai/dsh-workflow'

View File

@@ -4,7 +4,7 @@
* unions derived from them. Payloads are plain JSON by construction for structured clone. Both
* directions are closed engine protocols whose receivers use `assertNever`; generic typed senders
* make tag/payload mismatches compile-time errors rather than silently skipped messages.
* @module @deepseek-ai/dsh-workflow-workerthread/protocol
* @module @deepseek-ai/dsh-workflow-worker-thread/protocol
*/
import type { WorkflowAgentEndInfo, WorkflowAgentInfo, WorkflowResult } from '@deepseek-ai/dsh-workflow'

View File

@@ -5,7 +5,7 @@
* run, and the vm is not a security boundary. The worker provides host-loop isolation and
* forced termination, not hostile-value containment. See
* .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md for the isolation rationale.
* @module @deepseek-ai/dsh-workflow-workerthread/realm
* @module @deepseek-ai/dsh-workflow-worker-thread/realm
*/
/** Thrown by {@link materializeFromRealm}; the caller wraps it into the right `WorkflowError` code. */

View File

@@ -9,7 +9,7 @@
* per-item nulls. Every returned promise has a rejection consumer so dropped script promises cannot
* kill the worker. A cancelled script that never settles emits nothing; the host force-settles the
* run within grace and terminates the thread.
* @module @deepseek-ai/dsh-workflow-workerthread/runtime
* @module @deepseek-ai/dsh-workflow-worker-thread/runtime
*/
import * as vm from 'node:vm'

View File

@@ -8,7 +8,7 @@
* The session announces ready and waits for `go`, so cancellation racing startup can prevent even
* the script's synchronous prefix. A cancel in place of `go` releases the gate into a cancelled
* drive without executing the body.
* @module @deepseek-ai/dsh-workflow-workerthread/session
* @module @deepseek-ai/dsh-workflow-worker-thread/session
*/
import type { MessagePort } from 'node:worker_threads'

View File

@@ -2,7 +2,7 @@
* Non-protocol wire vocabulary for the worker-thread engine: the `workerData` init payload and
* the child-port interfaces the worker-side runtime consumes. Host/worker messages are defined in
* `./protocol.ts`; transported child requests and results are plain JSON for structured clone.
* @module @deepseek-ai/dsh-workflow-workerthread/types
* @module @deepseek-ai/dsh-workflow-worker-thread/types
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm'

View File

@@ -2,7 +2,7 @@
* Single-statement worker entry that boots `runWorkerSession` on real `parentPort`. Logic remains in
* the session module for in-process MessageChannel coverage; importing this entry on the main thread
* exercises `requireParentPort`'s failure path.
* @module @deepseek-ai/dsh-workflow-workerthread/worker
* @module @deepseek-ai/dsh-workflow-worker-thread/worker
*/
import { parentPort, workerData } from 'node:worker_threads'

View File

@@ -22,11 +22,11 @@ describe.skipIf(!existsSync(builtIndex) || !existsSync(builtWorker))('built work
try {
await writeFile(driver, `
import { Context } from '@deepseek-ai/cordis'
import SubagentService from '@deepseek-ai/dsh-subagent'
import WorkerWorkflowEngine from '@deepseek-ai/dsh-workflow-workerthread'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import WorkerThreadWorkflowEngine from '@deepseek-ai/dsh-workflow-worker-thread'
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
let selectedStarts = 0
ctx.subagents.registerProvider({
name: 'built-selected',
@@ -41,8 +41,8 @@ ctx.subagents.registerProvider({
}
},
})
await ctx.plugin(WorkerWorkflowEngine, { provider: 'must-not-be-used' })
const run = ctx.workflows.start({
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'must-not-be-used' })
const run = ctx.workflowEngine.start({
script: "const value = await agent('answer', { schema: { type: 'object', properties: { answer: { type: 'number' } }, required: ['answer'] } }); return value.answer",
meta: { name: 'built-smoke', description: 'built worker smoke' },
subagentProvider: 'built-selected',

View File

@@ -3,20 +3,20 @@ 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 from '@deepseek-ai/dsh-subagent'
import * as spawn from '@deepseek-ai/dsh-subagent-spawn'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-inprocess'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import { STRUCTURED_OUTPUT_TOOL } from '@deepseek-ai/dsh-subagent-in-process-driver'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
import WorkerWorkflowEngine from '../src/index.ts'
import WorkerThreadWorkflowEngine 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)
@@ -36,15 +36,15 @@ 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(WorkerWorkflowEngine, {})
await ctx.plugin(WorkerThreadWorkflowEngine, {})
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(SessionId('parent'), { provider: 'mock', model: 'mock' })
return { ctx, parent, adapter }
}
describe('dsh-workflow-workerthread over the real in-process stack', () => {
describe('dsh-workflow-worker-thread over the real in-process stack', () => {
it('runs a two-stage workflow: a plain child, then a schema child through the structured runtime', async () => {
const { ctx, parent } = await setup([
textResponse('the file list is a.ts'),
@@ -57,7 +57,7 @@ describe('dsh-workflow-workerthread over the real in-process stack', () => {
expect(ctx.agents.get(agent.childId)).toBeDefined()
childIds.push(agent.childId)
})
const run = ctx.workflows.start({
const run = ctx.workflowEngine.start({
meta: { name: 'integration', description: 'plain + structured children' },
script: `phase('Read')
const prose = await agent('read the repo')
@@ -85,7 +85,7 @@ return { prose, verdict: judged.verdict, confidence: judged.confidence }`,
textResponse('prose only'),
textResponse('still prose after the nudge'),
])
const run = ctx.workflows.start({
const run = ctx.workflowEngine.start({
meta: { name: 'null-path', description: 'schema failure maps to null' },
script: `const judged = await agent('judge it', { schema: { type: 'object', properties: { v: { type: 'string' } } } })
return { got: judged === null ? 'null' : 'value' }`,

View File

@@ -7,9 +7,9 @@
import { expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
import WorkerWorkflowEngine from '../src/index.ts'
import WorkerThreadWorkflowEngine from '../src/index.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
// A fresh thread compiles the source runtime. Leave contention headroom on
@@ -18,7 +18,7 @@ vi.setConfig({ testTimeout: 30_000 })
it('runs the default config through the source worker', async () => {
const ctx = new Context()
const subagents = await ctx.plugin(SubagentService)
const subagents = await ctx.plugin(SubagentRuntime)
const provider: SubagentProvider = {
name: 'spawn',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
@@ -26,10 +26,10 @@ it('runs the default config through the source worker', async () => {
start: () => Promise.reject(new Error('source-worker compat script must not start a child')),
}
ctx.subagents.registerProvider(provider)
const engine = await ctx.plugin(WorkerWorkflowEngine, {})
const engine = await ctx.plugin(WorkerThreadWorkflowEngine, {})
const parent = { id: SessionId('workflow-compat-parent'), options: {} } as unknown as Agent
try {
const run = ctx.workflows.start({
const run = ctx.workflowEngine.start({
script: 'return 6 * 7',
meta: { name: 'source-worker-compat', description: 'exercise the unbuilt worker entry' },
parent,

View File

@@ -1,16 +1,16 @@
import { afterEach, describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import LlmService from '@deepseek-ai/dsh-llm'
import LlmRuntime from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import ToolRuntime from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import SubagentService from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn'
import WorkerWorkflowEngine from '../src/index.ts'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
import WorkerThreadWorkflowEngine from '../src/index.ts'
/**
* With-key e2e: a REAL script in a REAL worker thread
@@ -30,16 +30,16 @@ afterEach(async () => {
async function harness(): Promise<Context> {
const built = new Context()
await built.plugin(LlmService)
await built.plugin(LlmRuntime)
await built.plugin(SessionStore)
await built.plugin(SystemPrompt)
await built.plugin(ToolRegistry)
await built.plugin(ToolRuntime)
await built.plugin(AgentRegistry)
await built.plugin(AgentLoop, { agents: [] })
await built.plugin(LlmDeepSeek)
await built.plugin(SubagentService)
await built.plugin(SubagentRuntime)
await built.plugin(Spawn, { providerName: 'spawn' })
await built.plugin(WorkerWorkflowEngine, { provider: 'spawn' })
await built.plugin(WorkerThreadWorkflowEngine, { provider: 'spawn' })
return built
}
@@ -76,7 +76,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('worker workflow engine with-key
})
}
const run = ctx.workflows.start({ script: SCRIPT, meta: META, parent: parentHandle.agent })
const run = ctx.workflowEngine.start({ script: SCRIPT, meta: META, parent: parentHandle.agent })
const result = await run.result
await run.dispose()

View File

@@ -4,11 +4,11 @@ import type { Worker } from 'node:worker_threads'
import { Context } from '@deepseek-ai/cordis'
import Loader from '@deepseek-ai/cordis-plugin-loader'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import SubagentRuntime from '@deepseek-ai/dsh-subagent'
import type { SubagentCapabilities, SubagentProvider, SubagentResult, SubagentRun, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import type { WorkflowMeta, WorkflowResult, WorkflowResultInfo, WorkflowRun, WorkflowRunInfo } from '@deepseek-ai/dsh-workflow'
import * as workerEngineModule from '../src/index.ts'
import WorkerWorkflowEngine, { type Config } from '../src/index.ts'
import WorkerThreadWorkflowEngine, { type Config } from '../src/index.ts'
import { HostToWorkerType, WorkerToHostType } from '../src/protocol.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
@@ -47,7 +47,7 @@ interface ControlledRun {
}
/**
* A scripted in-test provider over the REAL SubagentService registry: `auto`
* A scripted in-test provider over the REAL SubagentRuntime registry: `auto`
* settles each run via the reply function on a microtask; `manual` piles runs
* up in `runs` for the test to settle. A run aborts (settles `aborted`) when
* the request signal fires, like the real in-process backends.
@@ -141,7 +141,7 @@ interface SetupOptions {
async function setup(options?: SetupOptions) {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const provider = new StubProvider(
'stub',
options?.manual ? undefined : options?.reply ?? (() => text('stub reply')),
@@ -154,7 +154,7 @@ async function setup(options?: SetupOptions) {
// A fixed concurrency ceiling: the auto-resolved default is machine-derived
// (cores - 2, floored at 1), so tests that expect N children in flight
// would wedge on small CI runners.
const engineFiber = await ctx.plugin(WorkerWorkflowEngine, { provider: 'stub', maxConcurrentAgents: 8, ...options?.config })
const engineFiber = await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'stub', maxConcurrentAgents: 8, ...options?.config })
return { ctx, provider, parent: fakeParent(), engineFiber }
}
@@ -165,7 +165,7 @@ function scripted(body: string, metaExtra?: Partial<WorkflowMeta>): { script: st
/** Start + await one run, disposing on the way out. */
async function run(ctx: Context, parent: Agent, source: { script: string; meta: WorkflowMeta }, args?: unknown): Promise<WorkflowResult> {
const handle = ctx.workflows.start({ ...source, parent, ...args !== undefined ? { args } : {} })
const handle = ctx.workflowEngine.start({ ...source, parent, ...args !== undefined ? { args } : {} })
try {
return await handle.result
} finally {
@@ -173,7 +173,7 @@ async function run(ctx: Context, parent: Agent, source: { script: string; meta:
}
}
describe('dsh-workflow-workerthread', () => {
describe('dsh-workflow-worker-thread', () => {
describe('script execution over a real worker thread', () => {
it('runs a script end-to-end: agent() text results, phases, log, args, return value, events', async () => {
const { ctx, parent, provider } = await setup({ reply: (_request, index) => text(`answer-${index}`) })
@@ -237,7 +237,7 @@ describe('dsh-workflow-workerthread', () => {
const selected = new StubProvider('selected', () => text('selected reply'))
ctx.subagents.registerProvider(selected)
const overridden = ctx.workflows.start({
const overridden = ctx.workflowEngine.start({
...scripted("return await agent('route this run')"),
parent,
subagentProvider: 'selected',
@@ -261,7 +261,7 @@ describe('dsh-workflow-workerthread', () => {
let run: WorkflowRun | undefined
let thrown: unknown
try {
run = ctx.workflows.start({
run = ctx.workflowEngine.start({
...scripted("return 'must not start'"),
parent,
subagentProvider,
@@ -287,7 +287,7 @@ describe('dsh-workflow-workerthread', () => {
const errors: unknown[] = []
for (const maxTotalAgents of [0, 1.5, Number.NaN, 3]) {
try {
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("return 'must not start'"),
parent,
maxTotalAgents,
@@ -311,7 +311,7 @@ describe('dsh-workflow-workerthread', () => {
it('enforces a per-run total-agent cap below the engine ceiling', async () => {
const { ctx, parent } = await setup({ config: { maxTotalAgents: 2 } })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("await agent('first'); await agent('second'); return 'unreachable'"),
parent,
maxTotalAgents: 1,
@@ -334,7 +334,7 @@ describe('dsh-workflow-workerthread', () => {
const { ctx, parent } = await setup({ config: { provider: 'nonexistent' } })
let thrown: unknown
try {
ctx.workflows.start({ ...scripted("return 'must not start'"), parent })
ctx.workflowEngine.start({ ...scripted("return 'must not start'"), parent })
} catch (error: unknown) {
thrown = error
}
@@ -351,7 +351,7 @@ describe('dsh-workflow-workerthread', () => {
ctx.on('workflow/agent-end', (_info, agent) => { order.push(`end:${agent.outcome}`) })
ctx.on('workflow/end', () => { order.push('run-end') })
const handle = ctx.workflows.start({ ...scripted("return await agent('p')"), parent })
const handle = ctx.workflowEngine.start({ ...scripted("return await agent('p')"), parent })
await waitFor(() => { expect(provider.runs.length).toBe(1) })
const early = text('accepted value')
provider.runs[0]!.settle(early)
@@ -372,7 +372,7 @@ describe('dsh-workflow-workerthread', () => {
const lifecycle: string[] = []
ctx.on('workflow/agent-start', () => { lifecycle.push('start') })
ctx.on('workflow/agent-end', (_info, agent) => { lifecycle.push(`end:${agent.outcome}`) })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("try { await agent('p'); return 'unreachable' } catch (e) { return { code: e.code, message: e.message } }"),
parent,
})
@@ -404,7 +404,7 @@ describe('dsh-workflow-workerthread', () => {
ctx.on('workflow/agent-start', () => { lifecycle.push('start') })
ctx.on('workflow/agent-end', () => { lifecycle.push('end') })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("try { await agent('p'); return 'unreachable' } catch (e) { return { code: e.code, message: e.message } }"),
parent,
})
@@ -433,7 +433,7 @@ describe('dsh-workflow-workerthread', () => {
ctx.on('workflow/agent-start', () => { lifecycle.push('start') })
ctx.on('workflow/agent-end', () => { lifecycle.push('end') })
const handle = ctx.workflows.start({ ...scripted("return await agent('pending')"), parent })
const handle = ctx.workflowEngine.start({ ...scripted("return await agent('pending')"), parent })
await waitFor(() => { expect(provider.runs.length).toBe(1) })
const disposal = handle.dispose()
await waitFor(() => {
@@ -454,7 +454,7 @@ describe('dsh-workflow-workerthread', () => {
it('a child result REJECTION crosses back as a fatal AGENT_RESULT error (a broken provider is not a failed child)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const provider: SubagentProvider = {
name: 'rejecting',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false },
@@ -467,7 +467,7 @@ describe('dsh-workflow-workerthread', () => {
}),
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(WorkerWorkflowEngine, { provider: 'rejecting', maxConcurrentAgents: 2 })
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'rejecting', maxConcurrentAgents: 2 })
const result = await run(ctx, fakeParent(), scripted(`
try { await agent('p'); return 'unreachable' } catch (e) { return { name: e.name, code: e.code, fatal: e.fatal, message: e.message } }
`))
@@ -513,7 +513,7 @@ describe('dsh-workflow-workerthread', () => {
it('a child whose dispose() throws synchronously cannot wedge the script (the host acks anyway)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const provider: SubagentProvider = {
name: 'bad-dispose',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false },
@@ -527,7 +527,7 @@ describe('dsh-workflow-workerthread', () => {
}),
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(WorkerWorkflowEngine, { provider: 'bad-dispose', maxConcurrentAgents: 2 })
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'bad-dispose', maxConcurrentAgents: 2 })
const result = await run(ctx, fakeParent(), scripted("return await agent('p')"))
expect(result.stopReason).toBe('completed')
expect(result.value).toBe('fine')
@@ -535,7 +535,7 @@ describe('dsh-workflow-workerthread', () => {
it('a child dispose() rejecting an UNRENDERABLE value still acks — the containment warn is total', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const provider: SubagentProvider = {
name: 'coercion-trap-dispose',
capabilities: { outputSchema: true, depthLimit: true, toolFilter: true, persona: false },
@@ -553,7 +553,7 @@ describe('dsh-workflow-workerthread', () => {
}),
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(WorkerWorkflowEngine, { provider: 'coercion-trap-dispose', maxConcurrentAgents: 2 })
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'coercion-trap-dispose', maxConcurrentAgents: 2 })
const result = await run(ctx, fakeParent(), scripted("return await agent('p')"))
expect(result.stopReason).toBe('completed')
expect(result.value).toBe('fine')
@@ -604,12 +604,12 @@ describe('dsh-workflow-workerthread', () => {
it('start() throws synchronously for invalid meta data or an unparseable body (host-side pre-checks)', async () => {
const { ctx, parent } = await setup()
// Meta is DATA — shape violations reject loud, every one named.
expect(() => ctx.workflows.start({ script: 'return 1', meta: { name: '', description: 'd' }, parent })).toThrow(/meta\.name must be a non-empty string/)
expect(() => ctx.workflows.start({ script: 'return 1', meta: { name: 'x', description: 'd', extra: 1 } as unknown as WorkflowMeta, parent })).toThrow(/META_INVALID|not a recognized field/)
expect(() => ctx.workflows.start({ ...scripted('return ((('), parent })).toThrow(/does not parse/)
expect(() => ctx.workflowEngine.start({ script: 'return 1', meta: { name: '', description: 'd' }, parent })).toThrow(/meta\.name must be a non-empty string/)
expect(() => ctx.workflowEngine.start({ script: 'return 1', meta: { name: 'x', description: 'd', extra: 1 } as unknown as WorkflowMeta, parent })).toThrow(/META_INVALID|not a recognized field/)
expect(() => ctx.workflowEngine.start({ ...scripted('return ((('), parent })).toThrow(/does not parse/)
// The likeliest authoring slip — a Claude Code-style meta header in the
// body — gets a pointed message, not a bare SyntaxError.
expect(() => ctx.workflows.start({ ...scripted("export const meta = { name: 'x', description: 'd' }\nreturn 1"), parent })).toThrow(/meta rides the `meta` request field/)
expect(() => ctx.workflowEngine.start({ ...scripted("export const meta = { name: 'x', description: 'd' }\nreturn 1"), parent })).toThrow(/meta rides the `meta` request field/)
})
it('cancel() aborts in-flight children (signal AND cancel RPC) and settles the run cancelled', async () => {
@@ -618,7 +618,7 @@ describe('dsh-workflow-workerthread', () => {
ctx.on('workflow/agent-end', (_info, agent) => { ends.push(agent) })
const runEnds: WorkflowResultInfo[] = []
ctx.on('workflow/end', (_info, result) => { runEnds.push(result) })
const handle = ctx.workflows.start({ ...scripted("return await agent('long job')"), parent })
const handle = ctx.workflowEngine.start({ ...scripted("return await agent('long job')"), parent })
await waitFor(() => { expect(provider.runs.length).toBe(1) })
handle.cancel('user stopped it')
const result = await handle.result
@@ -638,7 +638,7 @@ describe('dsh-workflow-workerthread', () => {
controller.abort()
const logs: string[] = []
ctx.on('workflow/log', (_info, message) => { logs.push(message) })
const handle = ctx.workflows.start({ ...scripted("log('ran')\nreturn 123"), parent, signal: controller.signal })
const handle = ctx.workflowEngine.start({ ...scripted("log('ran')\nreturn 123"), parent, signal: controller.signal })
const result = await handle.result
expect(result.stopReason).toBe('cancelled')
expect(result.value).toBeNull()
@@ -649,7 +649,7 @@ describe('dsh-workflow-workerthread', () => {
it('cancel() right after start() cancels before the body runs; the signal aborting mid-run cancels like cancel()', async () => {
const { ctx, parent, provider } = await setup({ manual: true })
const first = ctx.workflows.start({ ...scripted("return await agent('never')"), parent })
const first = ctx.workflowEngine.start({ ...scripted("return await agent('never')"), parent })
// No-reason cancel: the canonical default reason must ride the result.
first.cancel()
const firstResult = await first.result
@@ -659,7 +659,7 @@ describe('dsh-workflow-workerthread', () => {
await first.dispose()
const controller = new AbortController()
const second = ctx.workflows.start({ ...scripted("return await agent('job')"), parent, signal: controller.signal })
const second = ctx.workflowEngine.start({ ...scripted("return await agent('job')"), parent, signal: controller.signal })
await waitFor(() => { expect(provider.runs.length).toBe(1) })
controller.abort()
expect((await second.result).stopReason).toBe('cancelled')
@@ -671,7 +671,7 @@ describe('dsh-workflow-workerthread', () => {
const settledController = new AbortController()
const settledAdd = vi.spyOn(settledController.signal, 'addEventListener')
const settledRemove = vi.spyOn(settledController.signal, 'removeEventListener')
const completed = ctx.workflows.start({ ...scripted('return 123'), parent, signal: settledController.signal })
const completed = ctx.workflowEngine.start({ ...scripted('return 123'), parent, signal: settledController.signal })
const settledAbort = settledAdd.mock.calls.find(([type]) => type === 'abort')?.[1]
expect(typeof settledAbort).toBe('function')
@@ -687,7 +687,7 @@ describe('dsh-workflow-workerthread', () => {
const teardownController = new AbortController()
const teardownAdd = vi.spyOn(teardownController.signal, 'addEventListener')
const teardownRemove = vi.spyOn(teardownController.signal, 'removeEventListener')
const tornDown = manual.ctx.workflows.start({
const tornDown = manual.ctx.workflowEngine.start({
...scripted("return await agent('job')"),
parent: manual.parent,
signal: teardownController.signal,
@@ -709,7 +709,7 @@ describe('dsh-workflow-workerthread', () => {
// timing can hit reliably. (The closure runs only after `handle` below
// is initialized — the listener fires on the worker's first message.)
ctx.on('workflow/log', () => { handle.cancel('cancelled from the log listener') })
const handle = ctx.workflows.start({ ...scripted("log('mark')\nreturn await agent('late')"), parent })
const handle = ctx.workflowEngine.start({ ...scripted("log('mark')\nreturn await agent('late')"), parent })
const result = await handle.result
expect(result.stopReason).toBe('cancelled')
expect(provider.runs.length).toBe(0)
@@ -721,7 +721,7 @@ describe('dsh-workflow-workerthread', () => {
const narration: string[] = []
ctx.on('workflow/log', (_info, message) => { narration.push(message) })
ctx.on('workflow/phase', (_info, title) => { narration.push(`phase:${title}`) })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
// The sync spin keeps the worker's loop busy so the cancel message
// cannot be processed before the script settles `completed` — the
// worker posts a completed result that must LOSE to the in-flight
@@ -751,7 +751,7 @@ describe('dsh-workflow-workerthread', () => {
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: 50 } })
const runEnds: WorkflowResultInfo[] = []
ctx.on('workflow/end', (_info, result) => { runEnds.push(result) })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("await new Promise(() => {})\nreturn 'unreachable'"),
parent,
})
@@ -767,7 +767,7 @@ describe('dsh-workflow-workerthread', () => {
it('dispose() on a stuck script returns within the grace instead of hanging (result settles cancelled)', async () => {
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: 50 } })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("await new Promise(() => {})\nreturn 'unreachable'"),
parent,
})
@@ -780,7 +780,7 @@ describe('dsh-workflow-workerthread', () => {
it('dispose() is idempotent and settles cleanly after a completed run', async () => {
const { ctx, parent } = await setup()
const handle = ctx.workflows.start({ ...scripted('return 1'), parent })
const handle = ctx.workflowEngine.start({ ...scripted('return 1'), parent })
await handle.result
await handle.dispose()
await handle.dispose()
@@ -791,7 +791,7 @@ describe('dsh-workflow-workerthread', () => {
// apart from every other timeout in flight.
const GRACE = 44_444
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: GRACE } })
const handle = ctx.workflows.start({ ...scripted('return 1'), parent })
const handle = ctx.workflowEngine.start({ ...scripted('return 1'), parent })
await handle.result
const spy = vi.spyOn(globalThis, 'setTimeout')
try {
@@ -809,7 +809,7 @@ describe('dsh-workflow-workerthread', () => {
it('strays: children fired without await are aborted once the script settles, and dispose() waits for their disposal', async () => {
const { ctx, parent, provider } = await setup({ manual: true, disposeDelayMs: 40 })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
agent('stray')
return 'done without awaiting'
@@ -830,7 +830,7 @@ describe('dsh-workflow-workerthread', () => {
manual: true,
config: { provider: 'stub', disposeGraceMs: 30_000 },
})
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("agent('stray')\nawait new Promise(() => {})"),
parent,
})
@@ -855,7 +855,7 @@ describe('dsh-workflow-workerthread', () => {
it('the settle-reap fires the request signal too: a provider honoring ONLY the signal winds its stray down promptly', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const aborted: string[] = []
const provider: SubagentProvider = {
name: 'signal-only',
@@ -877,8 +877,8 @@ describe('dsh-workflow-workerthread', () => {
},
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(WorkerWorkflowEngine, { provider: 'signal-only', maxConcurrentAgents: 2 })
const handle = ctx.workflows.start({
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'signal-only', maxConcurrentAgents: 2 })
const handle = ctx.workflowEngine.start({
...scripted(`
agent('stray, never awaited')
return 'done'
@@ -905,7 +905,7 @@ describe('dsh-workflow-workerthread', () => {
ctx.on('workflow/end', () => {
cancellationAtWorkflowEnd = provider.runs[0]?.cancelled
})
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
agent('start-pending stray')
return 'done'
@@ -932,7 +932,7 @@ describe('dsh-workflow-workerthread', () => {
manual: true,
onChildAbortString: (_reason, index) => { if (index === 0) signalAborts += 1 },
})
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("agent('stray')\nawait new Promise(() => {})"),
parent,
})
@@ -960,7 +960,7 @@ describe('dsh-workflow-workerthread', () => {
manual: true,
config: { provider: 'stub', maxConcurrentAgents: 2, disposeGraceMs: 100 },
})
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
// Let child-start cross, then make the worker unable to process its
// Cancel message. Grace settles the result and terminates the thread;
// that exit must independently own the host registry's disposal pass.
@@ -993,7 +993,7 @@ describe('dsh-workflow-workerthread', () => {
disposeDelayMs: 40,
config: { provider: 'stub', maxConcurrentAgents: 8, disposeGraceMs: 400 },
})
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
// Same shape as the wedged-cancel test above: the child's start RPC
// reaches the host, then the script seizes its worker's loop, so the
// worker can relay NO dispose RPC — the host's own dispose() drive is
@@ -1024,7 +1024,7 @@ describe('dsh-workflow-workerthread', () => {
it('a live child disposed by the dispose() drive is disposed ONCE, and the worker\'s late dispose RPC still gets its ack (the script settles, not the grace)', async () => {
const { ctx, parent, provider } = await setup({ manual: true })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
await agent('long child')
return 'unreachable'
@@ -1055,7 +1055,7 @@ describe('dsh-workflow-workerthread', () => {
order.push(`end:${agent.seq}`)
})
ctx.on('workflow/end', () => { order.push('run-end') })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
// 'slow' starts and its agent-start crosses to observers (the awaited
// 'fast' call keeps the worker loop turning), then the script seizes
// the loop: the wedged worker can never author slow's agent-end —
@@ -1096,7 +1096,7 @@ describe('dsh-workflow-workerthread', () => {
order.push(`end:${agent.seq}`)
})
ctx.on('workflow/end', () => { order.push('run-end') })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("await parallel([() => agent('a'), () => agent('b')])\nreturn 'unreachable'"),
parent,
})
@@ -1118,7 +1118,7 @@ describe('dsh-workflow-workerthread', () => {
const { ctx, parent, provider } = await setup({ manual: true })
const phases: string[] = []
ctx.on('workflow/phase', (_info, title) => { phases.push(title) })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted('await new Promise(() => {})'),
parent,
})
@@ -1150,7 +1150,7 @@ describe('dsh-workflow-workerthread', () => {
it('refuses and disposes a provider run that becomes ready after its real worker dies', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
const requested = Promise.withResolvers<SubagentStartRequest>()
const ready = Promise.withResolvers<SubagentRun>()
let disposeCalls = 0
@@ -1168,12 +1168,12 @@ describe('dsh-workflow-workerthread', () => {
},
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(WorkerWorkflowEngine, { provider: 'late-ready', maxConcurrentAgents: 1 })
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'late-ready', maxConcurrentAgents: 1 })
const lifecycle: string[] = []
ctx.on('workflow/agent-start', () => { lifecycle.push('start') })
ctx.on('workflow/agent-end', () => { lifecycle.push('end') })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("return await agent('pending startup')"),
parent: fakeParent(),
})
@@ -1212,7 +1212,7 @@ describe('dsh-workflow-workerthread', () => {
it('a worker that exits before settling reports an error result and reaps its children', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
await ctx.plugin(SubagentRuntime)
// The child's dispose() REJECTS on top of the worker death: the reap
// must contain it (warn, not crash) while still emptying the registry.
const signalAborts: unknown[] = []
@@ -1236,12 +1236,12 @@ describe('dsh-workflow-workerthread', () => {
},
}
ctx.subagents.registerProvider(provider)
await ctx.plugin(WorkerWorkflowEngine, { provider: 'doomed', maxConcurrentAgents: 2 })
await ctx.plugin(WorkerThreadWorkflowEngine, { provider: 'doomed', maxConcurrentAgents: 2 })
const runEnds: WorkflowResultInfo[] = []
ctx.on('workflow/end', (_info, result) => { runEnds.push(result) })
const childStarted = Promise.withResolvers<undefined>()
ctx.on('workflow/agent-start', () => { childStarted.resolve(undefined) })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted("return await agent('doomed')"),
parent: fakeParent(),
})
@@ -1267,7 +1267,7 @@ describe('dsh-workflow-workerthread', () => {
it('an uncaught exception inside the worker surfaces as an error result and reaps the in-flight child', async () => {
const { ctx, parent, provider } = await setup({ manual: true })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
agent('in flight when the worker dies')
const proc = ${ESCAPE}
@@ -1301,7 +1301,7 @@ describe('dsh-workflow-workerthread', () => {
order.push(`end:${agent.seq}`)
})
ctx.on('workflow/end', () => { order.push('run-end') })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
const p = agent('slow')
await agent('fast')
@@ -1331,7 +1331,7 @@ describe('dsh-workflow-workerthread', () => {
// it has nowhere to go and must be dropped silently (the workerGone
// guard in post()).
const { ctx, parent, provider } = await setup({ disposeDelayMs: 300 })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
agent('stray, never awaited')
await new Promise(() => {})
@@ -1357,7 +1357,7 @@ describe('dsh-workflow-workerthread', () => {
it('a worker death AFTER a cancel reports cancelled, not error', async () => {
const { ctx, parent } = await setup({ config: { provider: 'stub', disposeGraceMs: 60_000 } })
const handle = ctx.workflows.start({
const handle = ctx.workflowEngine.start({
...scripted(`
log('armed')
await new Promise(() => {})
@@ -1384,8 +1384,8 @@ describe('dsh-workflow-workerthread', () => {
const { ctx, parent } = await setup()
let eventMeta: WorkflowRunInfo | undefined
ctx.on('workflow/start', (info) => { eventMeta = info })
const first = ctx.workflows.start({ ...scripted('return 1'), parent })
const second = ctx.workflows.start({ ...scripted('return 2'), parent })
const first = ctx.workflowEngine.start({ ...scripted('return 1'), parent })
const second = ctx.workflowEngine.start({ ...scripted('return 2'), parent })
expect(first.id).not.toBe(second.id)
expect(eventMeta!.meta).toBe(second.meta)
expect(second.meta.name).toBe('test-flow')
@@ -1394,21 +1394,21 @@ describe('dsh-workflow-workerthread', () => {
await second.dispose()
})
it('unregisters ctx.workflows when the engine fiber is disposed (HMR safety)', async () => {
it('unregisters ctx.workflowEngine when the engine fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SubagentService)
const fiber = await ctx.plugin(WorkerWorkflowEngine, {})
expect(ctx.get('workflows')).toBeDefined()
await ctx.plugin(SubagentRuntime)
const fiber = await ctx.plugin(WorkerThreadWorkflowEngine, {})
expect(ctx.get('workflowEngine')).toBeDefined()
await fiber.dispose()
expect(ctx.get('workflows')).toBeUndefined()
expect(ctx.get('workflowEngine')).toBeUndefined()
})
it('keeps a holder-owned run usable when the engine unloads before its child starts', async () => {
const { ctx, parent, provider, engineFiber } = await setup({ reply: () => text('survived reload') })
let handle!: ReturnType<typeof ctx.workflows.start>
let handle!: ReturnType<typeof ctx.workflowEngine.start>
const holder = await ctx.plugin(Object.assign((inner: Context) => {
handle = inner.workflows.start({ ...scripted("return await agent('after reload')"), parent })
}, { inject: ['workflows'] }))
handle = inner.workflowEngine.start({ ...scripted("return await agent('after reload')"), parent })
}, { inject: ['workflowEngine'] }))
try {
// A real worker cannot deliver child-start in the synchronous start()
@@ -1416,7 +1416,7 @@ describe('dsh-workflow-workerthread', () => {
// run belongs to `holder`, not to the engine fiber being reloaded.
expect(provider.runs).toHaveLength(0)
await engineFiber.dispose()
expect(ctx.get('workflows')).toBeUndefined()
expect(ctx.get('workflowEngine')).toBeUndefined()
await expect(handle.result).resolves.toEqual({
value: 'survived reload',
@@ -1432,11 +1432,11 @@ describe('dsh-workflow-workerthread', () => {
})
it('has the class-plugin export shape (default = the engine service class)', () => {
expect(workerEngineModule.default).toBe(WorkerWorkflowEngine)
expect('WorkerWorkflowEngine' in workerEngineModule).toBe(false)
expect(workerEngineModule.default).toBe(WorkerThreadWorkflowEngine)
expect('WorkerThreadWorkflowEngine' in workerEngineModule).toBe(false)
const loader = Object.create(Loader.prototype) as Loader
const unwrapped: unknown = loader.unwrapExports(workerEngineModule)
expect(unwrapped).toBe(WorkerWorkflowEngine)
expect(unwrapped).toBe(WorkerThreadWorkflowEngine)
})
})
})

View File

@@ -39,7 +39,7 @@
"path": "../workflow"
},
{
"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/workflow/workflow/README.md
README.md: af61308b99bffb290fe2a11cb6c3ccd228cbfaf6
README.zh.md: e81909bad68beede99066a7c65208735ededca75
README.md: cc2c24f62512273ea1657542d0d904b0b870e236
README.zh.md: 22bda1bd33bc6350c8ff51f27538258bee872fdb

View File

@@ -2,15 +2,15 @@
English | [中文](README.zh.md)
The workflow seam (`ctx.workflows`) executes a model-written orchestration script that can fan out subagents. The seam defines the script, run, result, error, and event contracts; an engine decides how to isolate and execute the script.
The workflow seam (`ctx.workflowEngine`) executes a model-written orchestration script that can fan out subagents. The seam defines the script, run, result, error, and event contracts; an engine decides how to isolate and execute the script.
`@deepseek-ai/dsh-workflow-workerthread` is the current engine and `@deepseek-ai/dsh-tool-workflow` is the model-facing consumer. A future process or sandbox engine can replace the implementation without changing the tool.
`@deepseek-ai/dsh-workflow-worker-thread` is the current engine and `@deepseek-ai/dsh-tool-workflow` is the model-facing consumer. A future process or sandbox engine can replace the implementation without changing the tool.
The package root is the Host face. The browser-safe `@deepseek-ai/dsh-workflow/types` subpath contains run identities, metadata, results, and observe-only lifecycle payloads without importing `Agent`, Cordis services, or Host context declarations; Host-only `WorkflowStartRequest` and `WorkflowRun` live behind the package root.
## Service and run contract
`WorkflowService.start(request): WorkflowRun` validates enough synchronously to reject a malformed meta block, unparseable script, unavailable provider route, or unsupported per-run limit before a run exists. Once returned, `WorkflowRun.result` never rejects: execution failures resolve with `stopReason: 'error'`, and cancellation resolves with `cancelled` within the engine's bounded grace.
`WorkflowEngine.start(request): WorkflowRun` validates enough synchronously to reject a malformed meta block, unparseable script, unavailable provider route, or unsupported per-run limit before a run exists. Once returned, `WorkflowRun.result` never rejects: execution failures resolve with `stopReason: 'error'`, and cancellation resolves with `cancelled` within the engine's bounded grace.
A run is holder-owned. Engine-plugin unload prevents new starts but does not revoke accepted runs. The holder must call `dispose()` on every path; disposal cancels remaining work and reaches or abandons quiescence within the documented bound.

View File

@@ -2,15 +2,15 @@
[English](README.md) | 中文
工作流 seam扩展点`ctx.workflows`)执行由模型编写、可扇出 subagent 的编排脚本。该 seam 定义脚本、运行、结果、错误和事件契约;引擎负责决定如何隔离并执行脚本。
工作流 seam扩展点`ctx.workflowEngine`)执行由模型编写、可扇出 subagent 的编排脚本。该 seam 定义脚本、运行、结果、错误和事件契约;引擎负责决定如何隔离并执行脚本。
`@deepseek-ai/dsh-workflow-workerthread` 是当前引擎,`@deepseek-ai/dsh-tool-workflow` 是面向模型的消费方。未来的进程或沙箱引擎可以替换实现,而无需更改工具。
`@deepseek-ai/dsh-workflow-worker-thread` 是当前引擎,`@deepseek-ai/dsh-tool-workflow` 是面向模型的消费方。未来的进程或沙箱引擎可以替换实现,而无需更改工具。
包根是 Host face。浏览器安全的 `@deepseek-ai/dsh-workflow/types` 子路径包含运行身份、元数据、结果和仅供观察的生命周期 payload不导入 `Agent`、Cordis service 或 Host Context 声明Host 专用的 `WorkflowStartRequest``WorkflowRun` 只从包根提供。
## 服务与运行契约
`WorkflowService.start(request): WorkflowRun` 会同步完成足够多的校验,在运行创建前拒绝格式错误的 meta 块、无法解析的脚本、不可用的提供方路由或不受支持的单次运行限制。返回后,`WorkflowRun.result` 绝不拒绝:执行失败以 `stopReason: 'error'` 兑现,取消则在引擎有限的宽限时间内以 `cancelled` 兑现。
`WorkflowEngine.start(request): WorkflowRun` 会同步完成足够多的校验,在运行创建前拒绝格式错误的 meta 块、无法解析的脚本、不可用的提供方路由或不受支持的单次运行限制。返回后,`WorkflowRun.result` 绝不拒绝:执行失败以 `stopReason: 'error'` 兑现,取消则在引擎有限的宽限时间内以 `cancelled` 兑现。
运行由持有方负责。引擎插件卸载会阻止新的启动,但不会撤销已接受的运行。持有方必须在每条路径上调用 `dispose()`dispose资源释放会取消剩余工作并在文档规定的期限内达到或放弃完全停稳。

View File

@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-workflow",
"description": "Workflow capability seam: ctx.workflows service, run vocabulary, and workflow/* events",
"description": "Workflow capability seam: ctx.workflowEngine service, run vocabulary, and workflow/* events",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"

View File

@@ -30,7 +30,7 @@ export type { WorkflowRun, WorkflowStartRequest } from './runtime-types.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
workflows: WorkflowService
workflowEngine: WorkflowEngine
}
interface Events {
@@ -90,7 +90,7 @@ declare module '@deepseek-ai/cordis' {
}
}
/** The full set of `workflow/*` event names {@link WorkflowService.emitWorkflowEvent} dispatches. */
/** The full set of `workflow/*` event names {@link WorkflowEngine.emitWorkflowEvent} dispatches. */
export type WorkflowEventName =
| 'workflow/start'
| 'workflow/phase'
@@ -154,9 +154,9 @@ export function isFatalWorkflowError(error: unknown): boolean {
* listener failures are contained, and `workflow/end` fires exactly once as the
* result settles.
*/
export abstract class WorkflowService extends Service {
export abstract class WorkflowEngine extends Service {
constructor(ctx: Context) {
super(ctx, 'workflows')
super(ctx, 'workflowEngine')
}
/**
@@ -200,4 +200,4 @@ function renderListenerError(error: unknown): string {
}
}
export default WorkflowService
export default WorkflowEngine

View File

@@ -9,11 +9,11 @@ import type {
WorkflowRunInfo,
} from '@deepseek-ai/dsh-workflow'
import * as WorkflowInvariant from '@deepseek-ai/dsh-workflow/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(InvariantService)
await ctx.plugin(InvariantRegistry)
await ctx.plugin(WorkflowInvariant)
return ctx
}

View File

@@ -1,21 +1,21 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import WorkflowServiceDefault, {
import WorkflowEngineDefault, {
isFatalWorkflowError,
WorkflowError,
WorkflowRunId,
WorkflowService,
WorkflowEngine,
} from '../src/index.ts'
import type { WorkflowRun, WorkflowRunInfo, WorkflowStartRequest } from '../src/index.ts'
/** A minimal concrete subclass exposing the protected emit helper for tests. */
class StubEngine extends WorkflowService {
class StubEngine extends WorkflowEngine {
start(request: WorkflowStartRequest): WorkflowRun {
void request
throw new Error('not under test')
}
emit(name: Parameters<WorkflowService['emitWorkflowEvent']>[0], ...args: unknown[]): void {
emit(name: Parameters<WorkflowEngine['emitWorkflowEvent']>[0], ...args: unknown[]): void {
this.emitWorkflowEvent(name, ...args)
}
}
@@ -43,12 +43,12 @@ describe('dsh-workflow (interface)', () => {
expect(isFatalWorkflowError('string')).toBe(false)
})
it('registers as ctx.workflows and unregisters when its fiber is disposed (HMR safety)', async () => {
it('registers as ctx.workflowEngine and unregisters when its fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(StubEngine)
expect(ctx.get('workflows')).toBeInstanceOf(StubEngine)
expect(ctx.get('workflowEngine')).toBeInstanceOf(StubEngine)
await fiber.dispose()
expect(ctx.get('workflows')).toBeUndefined()
expect(ctx.get('workflowEngine')).toBeUndefined()
})
it('emitWorkflowEvent dispatches to every listener with the payload tuple', async () => {
@@ -57,7 +57,7 @@ describe('dsh-workflow (interface)', () => {
const seen: unknown[][] = []
ctx.on('workflow/log', (info, message) => { seen.push([info, message]) })
ctx.on('workflow/agent-start', (info, agent) => { seen.push([info, agent]) })
const engine = ctx.workflows as StubEngine
const engine = ctx.workflowEngine as StubEngine
engine.emit('workflow/start', INFO)
engine.emit('workflow/log', INFO, 'hello')
engine.emit('workflow/agent-start', INFO, { seq: 1, label: 'l', childId: 'c' })
@@ -78,7 +78,7 @@ describe('dsh-workflow (interface)', () => {
// oxlint-disable-next-line typescript/no-misused-promises -- exercises rejected-listener containment
ctx.on('workflow/agent-start', async () => { throw new Error('async observer failed') })
ctx.on('workflow/agent-start', (_info, agent) => { seen.push(agent.label) })
const engine = ctx.workflows as StubEngine
const engine = ctx.workflowEngine as StubEngine
const payload = { seq: 1, label: 'original', childId: 'c' }
engine.emit('workflow/start', INFO)
engine.emit('workflow/agent-start', INFO, payload)
@@ -96,7 +96,7 @@ describe('dsh-workflow (interface)', () => {
const reached: string[] = []
ctx.on('workflow/phase', () => { throw new Error('bad listener') })
ctx.on('workflow/phase', (_info, title) => { reached.push(title) })
const engine = ctx.workflows as StubEngine
const engine = ctx.workflowEngine as StubEngine
engine.emit('workflow/start', INFO)
expect(() => { engine.emit('workflow/phase', INFO, 'Scan') }).not.toThrow()
engine.emit('workflow/end', INFO, { stopReason: 'completed', agentsStarted: 0 })
@@ -114,7 +114,7 @@ describe('dsh-workflow (interface)', () => {
throw { toString: () => { throw new Error('coercion trap') } }
})
ctx.on('workflow/phase', (_info, title) => { reached.push(title) })
const engine = ctx.workflows as StubEngine
const engine = ctx.workflowEngine as StubEngine
engine.emit('workflow/start', INFO)
expect(() => { engine.emit('workflow/phase', INFO, 'Scan') }).not.toThrow()
engine.emit('workflow/end', INFO, { stopReason: 'completed', agentsStarted: 0 })
@@ -124,6 +124,6 @@ describe('dsh-workflow (interface)', () => {
})
it('has the expected exports (default = the abstract service class)', () => {
expect(WorkflowServiceDefault).toBe(WorkflowService)
expect(WorkflowEngineDefault).toBe(WorkflowEngine)
})
})

View File

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