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:
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 Note(agent 决策记录)](../../../.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 Note(agent 决策记录)](../../../.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 和上一次成功交接,但不会重试;致命的工作流基础设施失败可能在该状态返回前结束。
|
||||
|
||||
@@ -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:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"path": "../workflow"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 和结果包络。
|
||||
|
||||
## 模型看到的内容
|
||||
|
||||
|
||||
@@ -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:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 } : {},
|
||||
|
||||
@@ -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/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"path": "../workflow"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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。
|
||||
@@ -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:^",
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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']
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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. */
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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',
|
||||
@@ -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' }`,
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -39,7 +39,7 @@
|
||||
"path": "../workflow"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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(资源释放)会取消剩余工作,并在文档规定的期限内达到或放弃完全停稳。
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"path": "../../llm/llm"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user