@deepseek-ai/dsh-tool-workflow 0.1.6-alpha.2 → 0.1.7-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/workflow/tool-workflow/README.md
5
- README.md: 4cf16a233637da3fb2384ba80cdba8adb0298e5a
6
- README.zh.md: cfbbdd74f14c1c6f8c8c97b2bb84366ff61bb4d1
5
+ README.md: ce2804d2db83e410edb248332c517200dd6963dc
6
+ README.zh.md: 2ef39745a74eb6ab899108613c8cc8ac396f1352
package/README.md CHANGED
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
9
9
 
10
10
  ## Summary
11
11
 
12
- `dsh-tool-workflow` lets a model run a JavaScript orchestration script that delegates work to many subagents and returns the script's final JSON value. Use it only when the user explicitly requests a workflow or large multi-agent orchestration; use plain subagent calls for one or two delegations. The parent turn waits until every delegated task settles, and cancellation or abnormal completion returns an error rather than partial success. Deployments can rename the tool and cap rendered result text through `toolName` and `maxResultChars`.
12
+ `dsh-tool-workflow` lets a model run JavaScript orchestration that delegates to many subagents and returns a final JSON value. Use it only when the user explicitly requests a workflow or large multi-agent orchestration; prefer plain subagent calls for one or two delegations. Foreground execution waits for all work; cancellation or abnormal completion returns an error rather than partial success. `run_in_background: true` returns an owned job id immediately and exposes live output. Deployments can rename the tool with `toolName` and cap rendered results with `maxResultChars`.
13
13
 
14
14
  ## Table of Contents
15
15
 
@@ -29,20 +29,25 @@ The `workflow` tool runs a model-authored orchestration script that fans work ou
29
29
 
30
30
  ### Calling the tool
31
31
 
32
- The model submits three parameters: `meta` (required identity data: `name`, `description`, and optional `whenToUse` and `phases`), `script` (required plain JavaScript body — no `export const meta` statement; the tool description carries the complete authoring contract), and `args` (optional JSON object exposed to the script as the `args` global; wrap a bare list in a field so the wire schema stays honest).
32
+ The model submits three parameters plus one flag: `meta` (required identity data: `name`, `description`, and optional `whenToUse` and `phases`), `script` (required plain JavaScript body — no `export const meta` statement; the tool description carries the complete authoring contract), `args` (optional JSON object exposed to the script as the `args` global; wrap a bare list in a field so the wire schema stays honest), and `run_in_background` (optional; present only while `enableRunInBackground` holds).
33
33
 
34
- Success returns the canonical envelope `{ runId, agentsStarted, result }`, rendered to the model as `workflow "<name>" completed (<count> agent<optional-s>).` followed by `Return value:` and the pretty-printed JSON. A workflow that cannot start — a script parse or meta validation failure — returns an error the model can correct from. Cancellation and execution failures return `Error: workflow run was cancelled` or `Error: workflow run failed: <error>`; partial output is never reported as success.
34
+ A foreground success returns the envelope `{ kind: 'foreground', runId, agentsStarted, result }`, rendered to the model as `workflow "<name>" completed (<count> agent<optional-s>).` followed by `Return value:` and the pretty-printed JSON. A workflow that cannot start — a script parse or meta validation failure — returns an error the model can correct from. Cancellation and execution failures return `Error: workflow run was cancelled` or `Error: workflow run failed: <error>`; partial output is never reported as success.
35
35
 
36
36
  ### What to expect during a run
37
37
 
38
38
  While the script runs, the parent turn waits: the tool starts the run, awaits its result, and always disposes it, so the script and its children reach quiescence on every path — including cancellation, which is bridged from the parent step's abort signal. The model sees one final outcome, never intermediate child messages; the children's own work stays out of the parent conversation.
39
39
 
40
+ ### Background runs
41
+
42
+ `run_in_background: true` returns `{ kind: 'background', jobId, runId }` immediately: the run is registered on `ctx.jobs` as an owned `workflow` job, so the session-header job list streams its `phase()`, `log()`, and member lifecycle lines live from the job's output ring, and the row's progress line tracks the current phase. No tool-step signal reaches the run — `job_kill`, the list's stop control, and owner teardown are what cancel it. Settlement is the job's settlement: a completed run carries the same rendered return value as the job's result (the completion notice announces it, the model's first `job_output` after settlement carries it once), a cancelled run settles `killed` with the kill reason, and a failed run settles `failed` with the script's failure message. Without a live job registry and a controller serving the caller the call fails, naming the missing composition pieces.
43
+
40
44
  ### Config
41
45
 
42
46
  | Field | Default | Meaning |
43
47
  |---|---|---|
44
48
  | `toolName` | `workflow` | The model-facing tool name to register. |
45
49
  | `maxResultChars` | `50000` | Rendered-result ceiling; longer JSON is truncated with a notice. |
50
+ | `enableRunInBackground` | `true` | Expose `run_in_background`; disabled calls are also rejected. |
46
51
 
47
52
  The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-workflow) is the exhaustive source for every accepted field.
48
53
 
@@ -62,12 +67,18 @@ The consumer owns the model-facing schema, the `tool:<toolName>` system-prompt g
62
67
 
63
68
  ### Run lifecycle
64
69
 
65
- `execute` starts the run and awaits `run.result` inside a `try/finally` that always disposes the run. `exec.signal` is bridged to `run.cancel()`, including the already-aborted-before-start case. A non-`completed` stop reason maps to an `isError` result reporting the reason; completion renders `{ runId, agentsStarted, result }`, with the Native renderer truncating only that projection at `maxResultChars`.
70
+ `execute` starts the run and awaits `run.result` inside a `try/finally` that always disposes the run. `exec.signal` is bridged to `run.cancel()`, including the already-aborted-before-start case. A non-`completed` stop reason maps to an `isError` result reporting the reason; completion renders `{ kind, runId, agentsStarted, result }`, with the Native renderer truncating only that projection at `maxResultChars`.
71
+
72
+ ### Background lifecycle
73
+
74
+ A background call registers the run through `jobs.start` inside the job starter, so a synchronous engine rejection registers nothing and admission preflight runs before the engine spawns. The job's `done` chains from `run.result`: dispose (a disposal failure is warned, never rejected into the registry), stop the mirrors, then map the stop reason onto the job outcome. The ring mirror (`src/record.ts`) subscribes `workflow/phase`, `workflow/log`, and member events once per plugin and routes them into the tracked runs' `JobHandle` faces (`append` for lines, `updateProgress` for the phase); a straggling event after settlement finds no tracked run, and an append against a settled job drops inside the registry.
66
75
 
67
76
  ### Durable session records
68
77
 
69
78
  For a root transport execution (`exec.parent` absent), the tool projects the run into the calling Agent's Session with four log-only events: run-start after `start()` returns, member starts and endings filtered by `run.id`, then run-end only after the result is available and disposal reaches quiescence. Nested transport calls execute normally but write no record. The first failed Session append disables later recording for that run with one warning, leaving either no record or a legal continuous prefix without changing the tool result or cleanup. The package invariant rejects duplicate starts, unpaired members, terminal events with open members, and updates after run-end on both cold load and live append, while accepting missing terminal suffixes.
70
79
 
80
+ The engine's `workflow/phase` and `workflow/log` events have no per-line durable surface from this tool: the session log deliberately records run and member lifecycle only, and the Web transcript derives from those records. A background run's lines reach a human through the job observation record instead, which is transient by design.
81
+
71
82
  ### Render intent
72
83
 
73
84
  Decided up front per the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md): a `generic` card titled `workflow: <meta.name>`, read directly from `args.meta.name` — presentation is a pure function of args — with the script text carried as `rawInput`. The result keeps the generic card.
@@ -76,7 +87,8 @@ Decided up front per the [render-intent Agent Note](../../../.agents/notes/imple
76
87
 
77
88
  | File | Role |
78
89
  |---|---|
79
- | [`src/index.ts`](src/index.ts) | Plugin entry: tool registration, run lifecycle, recorder wiring |
90
+ | [`src/index.ts`](src/index.ts) | Plugin entry: tool registration, run lifecycle, background job registration, recorder wiring |
91
+ | [`src/record.ts`](src/record.ts) | Background runs' live-progress mirror into the job's output ring |
80
92
  | [`src/types.ts`](src/types.ts) | The four log-only record event payloads and their `SessionEventMap` declaration |
81
93
  | [`src/invariant.ts`](src/invariant.ts) | Invariant companion: durable workflow-record protocol validation |
82
94
 
@@ -139,7 +151,7 @@ Prefix-stable while `toolName`, definition, and visibility are unchanged. Renami
139
151
 
140
152
  #### What the model sees
141
153
 
142
- The full model-written script, metadata, and args remain in the assistant tool call. Success is exactly `workflow "<name>" completed (<count> agent<optional-s>).`, newline, `Return value:`, newline, and pretty-printed data-dependent JSON; a cap adds `… [truncated: <omitted> more characters]` on a new line. Failures are exactly `Error: workflow run was cancelled`, optionally suffixed ` (<error>)`, `Error: workflow run failed: <error-or-unknown error>`, or defensively `Error: workflow run ended abnormally (<reason>)`; a call without an owning agent becomes `Error: workflow tool requires a calling agent (exec.agent was undefined)`. Intermediate child messages are omitted.
154
+ The full model-written script, metadata, and args remain in the assistant tool call. A foreground success is exactly `workflow "<name>" completed (<count> agent<optional-s>).`, newline, `Return value:`, newline, and pretty-printed data-dependent JSON; a cap adds `… [truncated: <omitted> more characters]` on a new line. A background acceptance is exactly `workflow "<name>" started in the background as job <jobId>. Its return value arrives with the completion notice; check on it with job_output, stop it with job_kill.`, and the same rendered value later reaches the model through the job's completion notice and `job_output`. Failures are exactly `Error: workflow run was cancelled`, optionally suffixed ` (<error>)`, `Error: workflow run failed: <error-or-unknown error>`, or defensively `Error: workflow run ended abnormally (<reason>)`; a call without an owning agent becomes `Error: workflow tool requires a calling agent (exec.agent was undefined)`. Intermediate child messages are omitted.
143
155
 
144
156
  #### Token effect
145
157
 
@@ -156,10 +168,11 @@ Append-only; newly visible content follows the reusable request prefix and does
156
168
 
157
169
  These limits define what the tool does not yet support. They are current constraints, not a task backlog.
158
170
 
159
- - **The parent turn blocks until the whole workflow settles** — there is no background start/poll API, and cancellation discards partial output as an error.
171
+ - **A background run reports no intermediate value to the model** — `job_output` before settlement returns status only; the return value arrives whole at completion, and cancellation still discards partial output.
160
172
  - **`args` must be an object and Native result text is bounded** — callers wrap top-level arrays and scalars in a field; the canonical workflow result stays complete, while JSON beyond `maxResultChars` is truncated in the model-facing projection rather than stored behind a retrieval handle.
161
173
  - **Workflow policy is fixed per tool registration** — provider selection, caps, and tool name are deployment config, not model-call arguments.
162
174
  - **Durable records are top-level and observational** — nested PTC mode dispatches are not recorded, and a recording failure intentionally degrades to an incomplete prefix rather than changing execution.
175
+ - **No recorded-session scenario replays a background run yet** — unit and real-engine composition suites cover the path; the snapshot tree pins only the schema and prompt text.
163
176
 
164
177
  <a id="dev-note"></a>
165
178
  ### Dev Note
@@ -169,6 +182,6 @@ These limits define what the tool does not yet support. They are current constra
169
182
 
170
183
  This Dev Note is working context for maintainers: open directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
171
184
 
172
- Open directions: a background start/poll route so the parent turn does not block; storing truncated JSON behind a retrieval handle instead of clipping the projection; recording nested dispatches beyond the top level.
185
+ Open directions: storing truncated JSON behind a retrieval handle instead of clipping the projection; recording nested dispatches beyond the top level; a recorded-session scenario for the background path.
173
186
 
174
187
  </details>
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@ kind: "package-reference"
9
9
 
10
10
  ## 概述
11
11
 
12
- `dsh-tool-workflow` 让模型运行 JavaScript 编排脚本,把工作委派给多个 subagent,并返回脚本的最终 JSON 值。仅当用户明确要求工作流或大型多 agent(智能体)编排时使用;一两项委派应使用普通 subagent 调用。父级轮次会等待所有委派任务结束;取消或异常完成会返回错误,而不是部分成功。部署方可以通过 `toolName` 重命名工具,并通过 `maxResultChars` 限制渲染结果文本。
12
+ `dsh-tool-workflow` 让模型运行 JavaScript 编排,将工作委派给多个 subagent,并返回最终 JSON 值。仅当用户明确要求工作流或大型多 agent(智能体)编排时使用;一两项委派应优先使用普通 subagent 调用。前台执行等待所有工作结束;取消或异常完成返回错误,而不是部分成功。`run_in_background: true` 立即返回自有任务 id,并提供实时输出。部署方可以用 `toolName` 重命名工具,用 `maxResultChars` 限制渲染结果。
13
13
 
14
14
  ## 目录
15
15
 
@@ -29,20 +29,25 @@ kind: "package-reference"
29
29
 
30
30
  ### 调用工具
31
31
 
32
- 模型提交三个参数:`meta`(必需的身份数据:`name`、`description`,以及可选的 `whenToUse` 与 `phases`)、`script`(必需的纯 JavaScript 脚本体——不含 `export const meta` 语句;工具描述携带完整的编写约定)与 `args`(可选 JSON 对象,作为全局变量 `args` 向脚本公开;裸列表应包装到字段中,使协议 schema 如实表达形态)。
32
+ 模型提交三个参数外加一个开关:`meta`(必需的身份数据:`name`、`description`,以及可选的 `whenToUse` 与 `phases`)、`script`(必需的纯 JavaScript 脚本体——不含 `export const meta` 语句;工具描述携带完整的编写约定)、`args`(可选 JSON 对象,作为全局变量 `args` 向脚本公开;裸列表应包装到字段中,使协议 schema 如实表达形态),以及 `run_in_background`(可选;仅在 `enableRunInBackground` 生效时存在)。
33
33
 
34
- 成功返回规范包络 `{ runId, agentsStarted, result }`,向模型渲染为 `workflow "<name>" completed (<count> agent<optional-s>).`,后接 `Return value:` 与美化打印的 JSON。无法启动的工作流——脚本解析或 meta 校验失败——返回模型可以修正的错误。取消与执行失败返回 `Error: workflow run was cancelled` 或 `Error: workflow run failed: <error>`;部分输出绝不会被报告为成功。
34
+ 前台成功返回包络 `{ kind: 'foreground', runId, agentsStarted, result }`,向模型渲染为 `workflow "<name>" completed (<count> agent<optional-s>).`,后接 `Return value:` 与美化打印的 JSON。无法启动的工作流——脚本解析或 meta 校验失败——返回模型可以修正的错误。取消与执行失败返回 `Error: workflow run was cancelled` 或 `Error: workflow run failed: <error>`;部分输出绝不会被报告为成功。
35
35
 
36
36
  ### 运行期间的预期
37
37
 
38
38
  脚本运行期间,父级轮次会等待:工具启动运行、等待其结果,并始终对该运行执行 dispose(资源释放),因此脚本及其子 agent 在每条路径上完全停稳——包括从父级步骤中止信号桥接而来的取消。模型只看到最终结果,永远不会看到中间子 agent 消息;子 agent 自己的工作不会进入父级对话。
39
39
 
40
+ ### 后台运行
41
+
42
+ `run_in_background: true` 会立刻返回 `{ kind: 'background', jobId, runId }`:运行以自有 `workflow` 任务身份注册到 `ctx.jobs`,会话头部任务列表因此从该任务的输出环实时流式显示它的 `phase()`、`log()` 与成员生命周期行,行的进度行跟随当前阶段。没有任何工具步骤信号到达该运行——`job_kill`、列表里的停止控件与 owner 拆除才是取消它的途径。结算即任务的结算:完成的运行把同一份渲染后的返回值作为任务的 result 交出(完成通知宣布它,模型结算后的第一次 `job_output` 携带它一次),被取消的运行以 kill 原因结算为 `killed`,失败的运行以脚本的失败信息结算为 `failed`。没有活体任务注册表与服务于调用方的控制器时调用失败,并点名缺失的组合部件。
43
+
40
44
  ### 配置
41
45
 
42
46
  | 字段 | 默认值 | 含义 |
43
47
  |---|---|---|
44
48
  | `toolName` | `workflow` | 要注册的面向模型工具名称。 |
45
49
  | `maxResultChars` | `50000` | 渲染结果上限;更长的 JSON 会被截断并附上提示。 |
50
+ | `enableRunInBackground` | `true` | 公开 `run_in_background`;关闭后调用同样会被拒绝。 |
46
51
 
47
52
  生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-workflow)是每个受支持字段的穷尽式真源。
48
53
 
@@ -62,12 +67,18 @@ kind: "package-reference"
62
67
 
63
68
  ### 运行生命周期
64
69
 
65
- `execute` 启动运行,并在 `try/finally` 内等待 `run.result`;该结构总会对运行执行 dispose。`exec.signal` 会桥接到 `run.cancel()`,包括启动前已经中止的情况。非 `completed` 结束原因会映射为报告原因的 `isError` 结果;完成时渲染 `{ runId, agentsStarted, result }`,Native 渲染器只会在 `maxResultChars` 处截断该投影。
70
+ `execute` 启动运行,并在 `try/finally` 内等待 `run.result`;该结构总会对运行执行 dispose。`exec.signal` 会桥接到 `run.cancel()`,包括启动前已经中止的情况。非 `completed` 结束原因会映射为报告原因的 `isError` 结果;完成时渲染 `{ kind, runId, agentsStarted, result }`,Native 渲染器只会在 `maxResultChars` 处截断该投影。
71
+
72
+ ### 后台生命周期
73
+
74
+ 后台调用在任务 starter 内经 `jobs.start` 注册运行,因此引擎的同步拒绝什么都不会注册,准入预检也先于引擎生成执行。任务的 `done` 链接自 `run.result`:先 dispose(释放失败只告警,绝不 reject 进注册表),再停止镜像,然后把停止原因映射到任务结果。环镜像(`src/record.ts`)按插件订阅一次 `workflow/phase`、`workflow/log` 与成员事件,并把它们路由进被跟踪运行的 `JobHandle` 面(`append` 写行,`updateProgress` 写阶段);结算后的零星事件找不到被跟踪的运行,对已结算任务的 append 则在注册表内丢弃。
66
75
 
67
76
  ### 持久会话记录
68
77
 
69
78
  对于根 transport 执行(`exec.parent` 缺省),工具会用四个 log-only 事件把运行投影到调用方 agent 的会话:`start()` 返回后写 run-start,只记录 `run.id` 匹配的成员开始与结束,并且只在结果可用且 dispose 完全停稳后写 run-end。嵌套 transport 调用照常执行,但不写任何记录。会话追加操作首次失败后,本运行会停止后续记录并只告警一次,留下空记录或合法连续前缀,同时不改变工具结果和清理。包 invariant 会在冷加载与实时追加时拒绝重复 start、未配对成员、仍有开放成员的终点与 run-end 后更新,同时允许缺失终态后缀的连续前缀。
70
79
 
80
+ 引擎的 `workflow/phase` 与 `workflow/log` 事件在本工具没有逐行的持久面:会话日志刻意只记录 run 与成员生命周期,Web transcript 由这些记录派生。后台运行的这些行改经任务观察 record 抵达人类,而 record 的瞬态是设计使然。
81
+
71
82
  ### 渲染意图
72
83
 
73
84
  按[渲染意图 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.zh.md)预先确定:使用 `generic` 卡片,标题为 `workflow: <meta.name>`,直接从 `args.meta.name` 读取——呈现是参数的纯函数——脚本文本作为 `rawInput` 携带。结果继续使用 generic 卡片。
@@ -76,7 +87,8 @@ kind: "package-reference"
76
87
 
77
88
  | 文件 | 职责 |
78
89
  |---|---|
79
- | [`src/index.ts`](src/index.ts) | 插件入口:工具注册、运行生命周期、记录器接线 |
90
+ | [`src/index.ts`](src/index.ts) | 插件入口:工具注册、运行生命周期、后台任务注册、记录器接线 |
91
+ | [`src/record.ts`](src/record.ts) | 后台运行进任务输出环的实时进度镜像 |
80
92
  | [`src/types.ts`](src/types.ts) | 四个 log-only 记录事件 payload 及其 `SessionEventMap` 声明 |
81
93
  | [`src/invariant.ts`](src/invariant.ts) | 不变式配套入口:持久工作流记录协议校验 |
82
94
 
@@ -139,7 +151,7 @@ Use the <toolName> tool ONLY when the user explicitly asks for a workflow or for
139
151
 
140
152
  #### 模型看到什么
141
153
 
142
- 由模型编写的完整脚本、元数据与 args 会保留在 assistant 工具调用中。成功结果精确为 `workflow "<name>" completed (<count> agent<optional-s>).`、换行、`Return value:`、换行,以及美化打印且依赖数据的 JSON;达到上限时,会在新行添加 `… [truncated: <omitted> more characters]`。失败结果精确为 `Error: workflow run was cancelled`(可以追加后缀 ` (<error>)`)、`Error: workflow run failed: <error-or-unknown error>` 或防御性的 `Error: workflow run ended abnormally (<reason>)`;没有所属 agent 的调用变为 `Error: workflow tool requires a calling agent (exec.agent was undefined)`。中间子 agent 消息会被省略。
154
+ 由模型编写的完整脚本、元数据与 args 会保留在 assistant 工具调用中。前台成功结果精确为 `workflow "<name>" completed (<count> agent<optional-s>).`、换行、`Return value:`、换行,以及美化打印且依赖数据的 JSON;达到上限时,会在新行添加 `… [truncated: <omitted> more characters]`。后台受理结果精确为 `workflow "<name>" started in the background as job <jobId>. Its return value arrives with the completion notice; check on it with job_output, stop it with job_kill.`,同样渲染的值稍后经任务完成播报与 `job_output` 抵达模型。失败结果精确为 `Error: workflow run was cancelled`(可以追加后缀 ` (<error>)`)、`Error: workflow run failed: <error-or-unknown error>` 或防御性的 `Error: workflow run ended abnormally (<reason>)`;没有所属 agent 的调用变为 `Error: workflow tool requires a calling agent (exec.agent was undefined)`。中间子 agent 消息会被省略。
143
155
 
144
156
  #### Token 影响
145
157
 
@@ -156,10 +168,11 @@ Use the <toolName> tool ONLY when the user explicitly asks for a workflow or for
156
168
 
157
169
  这些限制说明该工具尚未支持什么。它们是当前约束,不是任务积压。
158
170
 
159
- - **父级轮次会阻塞到整个工作流结算**——没有后台启动/轮询接口,取消会丢弃局部输出并返回错误。
171
+ - **后台运行不向模型报告中间值**——结算前的 `job_output` 只返回状态;返回值在完成时整体送达,取消仍会丢弃局部输出。
160
172
  - **`args` 必须是对象,Native 结果文本有界**——调用方把顶层数组/标量包装到字段中;规范工作流结果保持完整,超过 `maxResultChars` 的 JSON 会在面向模型的投影中截断,而不是存储在检索句柄背后。
161
173
  - **每次工具注册的工作流策略固定**——提供方选择、上限与工具名称属于部署配置,不是模型调用参数。
162
174
  - **持久记录只覆盖顶层且只供观察**——嵌套 PTC mode dispatch 不记录;记录故障会刻意退化为不完整前缀,而不改变执行。
175
+ - **尚无回放后台运行的 recorded-session 场景**——单元与真实引擎组合套件覆盖该路径;快照树只钉住 schema 与提示词文本。
163
176
 
164
177
  <a id="dev-note"></a>
165
178
  ### 开发备注
@@ -169,6 +182,6 @@ Use the <toolName> tool ONLY when the user explicitly asks for a workflow or for
169
182
 
170
183
  本开发备注是维护者的工作上下文:尚未决定的开放方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码与相关 Agent Note 为准。
171
184
 
172
- 开放方向:让父级轮次不再阻塞的后台启动/轮询路径;把截断的 JSON 存储在检索句柄背后,而不是剪裁投影;记录超出顶层的嵌套 dispatch。
185
+ 开放方向:把截断的 JSON 存储在检索句柄背后,而不是剪裁投影;记录超出顶层的嵌套 dispatch;为后台路径补一个 recorded-session 场景。
173
186
 
174
187
  </details>
package/lib/index.js CHANGED
@@ -1,13 +1,58 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ //#region lib/types/record.js
4
+ /**
5
+ * Live-progress mirror for background workflow runs: streams the engine's
6
+ * `workflow/phase`, `workflow/log`, and member lifecycle events into the
7
+ * owning job's output ring as `log` chunks — observer-only narration the
8
+ * model's `job_output` never renders — and keeps the job's live progress
9
+ * line on the current phase. Appends against a settled job log and drop
10
+ * inside the registry, so a straggling event after settlement is harmless.
11
+ * @module @deepseek-ai/dsh-tool-workflow/record
12
+ */
13
+ /**
14
+ * Create the run-to-ring mirror and subscribe the engine's live progress
15
+ * events for the runs it tracks.
16
+ * @param ctx - plugin context whose event bus carries the `workflow/*` events.
17
+ * @returns the mirror taps the tool wires around each background run.
18
+ */
19
+ function createWorkflowRecordMirror(ctx) {
20
+ const active = /* @__PURE__ */ new Map();
21
+ ctx.on("workflow/phase", (info, title) => {
22
+ const job = active.get(info.id);
23
+ if (job === void 0) return;
24
+ job.updateProgress(title);
25
+ job.append(`▸ ${title}\n`, { channel: "log" });
26
+ });
27
+ ctx.on("workflow/log", (info, message) => {
28
+ active.get(info.id)?.append(`${message}\n`, { channel: "log" });
29
+ });
30
+ ctx.on("workflow/agent-start", (info, agent) => {
31
+ active.get(info.id)?.append(`agent #${agent.seq} ${agent.label} started\n`, { channel: "log" });
32
+ });
33
+ ctx.on("workflow/agent-end", (info, agent) => {
34
+ active.get(info.id)?.append(`agent #${agent.seq} ${agent.outcome}\n`, { channel: "log" });
35
+ });
36
+ return {
37
+ start(runId, job) {
38
+ active.set(runId, job);
39
+ },
40
+ stop(runId) {
41
+ active.delete(runId);
42
+ }
43
+ };
44
+ }
45
+ //#endregion
3
46
  //#region lib/types/index.js
4
47
  /**
5
48
  * The model-facing `workflow` tool: run a JavaScript orchestration script that fans out
6
49
  * subagents, and return the script's final value. It owns the model-facing schema and run lifecycle; script
7
50
  * parsing, execution, caps, and cancellation live behind `ctx.workflowEngine`
8
51
  * (`@deepseek-ai/dsh-workflow`), so a hardened engine swaps in without touching what the model
9
- * sees. Execution awaits `run.result` and always disposes the run; non-completed reasons become tool
10
- * errors, and background collection remains deferred. Presentation is an args-only generic card
52
+ * sees. Foreground execution awaits `run.result` and always disposes the run; non-completed reasons
53
+ * become tool errors. `run_in_background: true` instead registers the run as an owned `ctx.jobs` job
54
+ * and returns its id immediately — the job's output ring streams live progress, and the run's value
55
+ * arrives with the job's completion notice. Presentation is an args-only generic card
11
56
  * titled from `meta.name`. Explicit-ask usage guidance is registered as the tool's own prompt
12
57
  * section rather than deployment persona prose.
13
58
  * @module @deepseek-ai/dsh-tool-workflow
@@ -20,7 +65,8 @@ const inject = [
20
65
  ];
21
66
  const Config = z.object({
22
67
  toolName: z.string().default("workflow"),
23
- maxResultChars: z.natural().min(1).default(5e4)
68
+ maxResultChars: z.natural().min(1).default(5e4),
69
+ enableRunInBackground: z.boolean().default(true)
24
70
  });
25
71
  /** Render a contained recording failure without trusting the thrown value. */
26
72
  function renderRecordingError(error) {
@@ -89,7 +135,8 @@ function createWorkflowRecorder(ctx) {
89
135
  /**
90
136
  * The script-authoring contract, embedded in the tool description. This IS the
91
137
  * model-facing spec: the meta block, the hooks and their exact semantics, and
92
- * the supported schema subset.
138
+ * the supported schema subset. The closing execution sentence follows the
139
+ * composition: only a background-enabled tool describes `run_in_background`.
93
140
  */
94
141
  const DESCRIPTION = `Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.
95
142
 
@@ -103,7 +150,9 @@ Script-body hooks:
103
150
 
104
151
  Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item \`null\`.
105
152
 
106
- Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.`;
153
+ Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them.`;
154
+ const FOREGROUND_ONLY_CLOSING = " The run executes in the foreground: this call returns when the whole script finishes.";
155
+ const BACKGROUND_CLOSING = " The run executes in the foreground by default: this call returns when the whole script finishes. Set `run_in_background: true` for a long run: the call returns a job id immediately, the run keeps orchestrating in the background, and its return value arrives with the job's completion notice (check on it with `job_output`, stop it with `job_kill`).";
107
156
  /** The pending-state card: a generic card titled by the workflow's meta name. */
108
157
  function presentWorkflowCall(args) {
109
158
  return {
@@ -126,15 +175,98 @@ function stopReasonError(result) {
126
175
  default: return `workflow run ended abnormally (${String(result.stopReason)})`;
127
176
  }
128
177
  }
178
+ /**
179
+ * Map a settled background run onto the job outcome vocabulary. A completed
180
+ * run carries the rendered return value as the job's result; a
181
+ * cancelled run leaves the detail to the registry's kill-reason merge (the
182
+ * cancel reason it forwarded is the same string); an errored run fails with
183
+ * the script's failure message.
184
+ */
185
+ function jobOutcomeOf(result, name, maxChars) {
186
+ switch (result.stopReason) {
187
+ case "completed": return {
188
+ status: "completed",
189
+ detail: `${result.agentsStarted} agent${result.agentsStarted === 1 ? "" : "s"}`,
190
+ result: renderResult(name, result.agentsStarted, result.value, maxChars)
191
+ };
192
+ case "cancelled": return { status: "killed" };
193
+ case "error": return {
194
+ status: "failed",
195
+ detail: result.error ?? "unknown error"
196
+ };
197
+ /* v8 ignore start -- defensive: WorkflowStopReason is a closed union, exhaustive by construction; a future variant fails here loudly */
198
+ default: return {
199
+ status: "failed",
200
+ detail: `workflow run ended abnormally (${String(result.stopReason)})`
201
+ };
202
+ }
203
+ }
129
204
  /** Render the run's outcome text: the meta name, agent count, and the JSON value (capped). */
130
205
  function renderResult(name, agentsStarted, value, maxChars) {
131
206
  const rendered = JSON.stringify(value, null, 2);
132
207
  const clipped = rendered.length > maxChars ? `${rendered.slice(0, maxChars)}\n… [truncated: ${rendered.length - maxChars} more characters]` : rendered;
133
208
  return `workflow "${name}" completed (${agentsStarted} agent${agentsStarted === 1 ? "" : "s"}).\nReturn value:\n${clipped}`;
134
209
  }
210
+ /**
211
+ * Register a background run as an owned job. The engine run is started
212
+ * inside the job starter with no tool-step signal — the run belongs to the
213
+ * job, so a registry kill or owner teardown is what cancels it — and its
214
+ * settlement is the job's settlement: dispose, stop the mirrors, then map the
215
+ * stop reason onto the job outcome (a completed run's rendered return value
216
+ * rides `result` to the model's first read after settlement).
217
+ * @param ctx - plugin context (engine, optional jobs registry, logger).
218
+ * @param args - the validated tool call.
219
+ * @param parent - the calling agent; owns the job.
220
+ * @param recordsRun - whether this top-level call records durable run events.
221
+ * @param deps - the tool's recorder/mirror taps and the render cap.
222
+ * @returns the background result for the tool's output schema.
223
+ */
224
+ function startBackgroundRun(ctx, args, parent, recordsRun, deps) {
225
+ const jobs = ctx.get("jobs");
226
+ if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
227
+ let run;
228
+ return {
229
+ kind: "background",
230
+ jobId: jobs.start({
231
+ kind: "workflow",
232
+ label: args.meta.name,
233
+ owner: parent.id,
234
+ run: (job) => {
235
+ run = ctx.workflowEngine.start({
236
+ script: args.script,
237
+ meta: args.meta,
238
+ ...args.args !== void 0 ? { args: args.args } : {},
239
+ parent
240
+ });
241
+ deps.mirror.start(run.id, job);
242
+ if (recordsRun) deps.recorder.start(parent.session, run);
243
+ return {
244
+ cancel: (reason) => {
245
+ run.cancel(reason ?? "background workflow job killed");
246
+ },
247
+ done: run.result.then(async (result) => {
248
+ try {
249
+ await run.dispose();
250
+ } catch (error) {
251
+ ctx.logger.warn(`background workflow run ${run.id} dispose failed: ${String(error)}`);
252
+ }
253
+ deps.mirror.stop(run.id);
254
+ if (recordsRun) {
255
+ deps.recorder.finish(run.id, result.stopReason);
256
+ deps.recorder.abandon(run.id);
257
+ }
258
+ return jobOutcomeOf(result, args.meta.name, deps.maxResultChars);
259
+ })
260
+ };
261
+ }
262
+ }),
263
+ runId: run.id
264
+ };
265
+ }
135
266
  function apply(ctx, config) {
136
- const { toolName, maxResultChars } = config;
267
+ const { toolName, maxResultChars, enableRunInBackground } = config;
137
268
  const recorder = createWorkflowRecorder(ctx);
269
+ const mirror = createWorkflowRecordMirror(ctx);
138
270
  ctx.systemPrompt.section({
139
271
  name: `tool:${toolName}`,
140
272
  order: ctx.systemPrompt.getSectionOrder("TOOL_WORKFLOW"),
@@ -142,7 +274,7 @@ function apply(ctx, config) {
142
274
  });
143
275
  ctx.tools.register(defineTool({
144
276
  name: toolName,
145
- description: DESCRIPTION,
277
+ description: DESCRIPTION + (enableRunInBackground ? BACKGROUND_CLOSING : FOREGROUND_ONLY_CLOSING),
146
278
  parameters: {
147
279
  script: {
148
280
  type: "string",
@@ -202,13 +334,40 @@ function apply(ctx, config) {
202
334
  type: "object",
203
335
  additionalProperties: true,
204
336
  description: "Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {\"files\": [...]})."
205
- }
337
+ },
338
+ ...enableRunInBackground ? { run_in_background: {
339
+ type: "boolean",
340
+ description: "Run as a background job: return a job id immediately instead of waiting; the return value arrives with the completion notice."
341
+ } } : {}
206
342
  },
207
343
  output: {
208
- schema: {
344
+ schema: { oneOf: [{
345
+ type: "object",
346
+ additionalProperties: false,
347
+ properties: {
348
+ kind: {
349
+ type: "string",
350
+ required: true,
351
+ const: "background"
352
+ },
353
+ jobId: {
354
+ type: "string",
355
+ required: true
356
+ },
357
+ runId: {
358
+ type: "string",
359
+ required: true
360
+ }
361
+ }
362
+ }, {
209
363
  type: "object",
210
364
  additionalProperties: false,
211
365
  properties: {
366
+ kind: {
367
+ type: "string",
368
+ required: true,
369
+ const: "foreground"
370
+ },
212
371
  runId: {
213
372
  type: "string",
214
373
  required: true
@@ -222,15 +381,23 @@ function apply(ctx, config) {
222
381
  required: true
223
382
  }
224
383
  }
225
- },
384
+ }] },
226
385
  render: (args, value) => [{
227
386
  type: "text",
228
- text: renderResult(args.meta.name, value.agentsStarted, value.result, maxResultChars)
387
+ text: value.kind === "background" ? `workflow "${args.meta.name}" started in the background as job ${value.jobId}. Its return value arrives with the completion notice; check on it with job_output, stop it with job_kill.` : renderResult(args.meta.name, value.agentsStarted, value.result, maxResultChars)
229
388
  }]
230
389
  },
231
390
  async execute(args, exec) {
232
391
  const parent = exec.agent;
233
392
  if (!parent) throw new Error("workflow tool requires a calling agent (exec.agent was undefined)");
393
+ if (args.run_in_background === true) {
394
+ if (!enableRunInBackground) throw new Error("run_in_background is disabled for this tool");
395
+ return startBackgroundRun(ctx, args, parent, exec.parent === void 0, {
396
+ recorder,
397
+ mirror,
398
+ maxResultChars
399
+ });
400
+ }
234
401
  const run = ctx.workflowEngine.start({
235
402
  script: args.script,
236
403
  meta: args.meta,
@@ -250,6 +417,7 @@ function apply(ctx, config) {
250
417
  const error = stopReasonError(result);
251
418
  if (error !== void 0) throw new Error(error);
252
419
  return {
420
+ kind: "foreground",
253
421
  runId: run.id,
254
422
  agentsStarted: result.agentsStarted,
255
423
  result: result.value
@@ -3,14 +3,21 @@
3
3
  * subagents, and return the script's final value. It owns the model-facing schema and run lifecycle; script
4
4
  * parsing, execution, caps, and cancellation live behind `ctx.workflowEngine`
5
5
  * (`@deepseek-ai/dsh-workflow`), so a hardened engine swaps in without touching what the model
6
- * sees. Execution awaits `run.result` and always disposes the run; non-completed reasons become tool
7
- * errors, and background collection remains deferred. Presentation is an args-only generic card
6
+ * sees. Foreground execution awaits `run.result` and always disposes the run; non-completed reasons
7
+ * become tool errors. `run_in_background: true` instead registers the run as an owned `ctx.jobs` job
8
+ * and returns its id immediately — the job's output ring streams live progress, and the run's value
9
+ * arrives with the job's completion notice. Presentation is an args-only generic card
8
10
  * titled from `meta.name`. Explicit-ask usage guidance is registered as the tool's own prompt
9
11
  * section rather than deployment persona prose.
10
12
  * @module @deepseek-ai/dsh-tool-workflow
11
13
  */
12
14
  import type { Context } from '@deepseek-ai/cordis';
13
15
  import z from '@deepseek-ai/schemastery';
16
+ declare module '@deepseek-ai/dsh-jobs' {
17
+ interface JobKindMap {
18
+ workflow: 'workflow';
19
+ }
20
+ }
14
21
  export declare const name = "tool-workflow";
15
22
  export declare const inject: string[];
16
23
  /** Config: the model-facing tool name plus result rendering caps. */
@@ -19,6 +26,14 @@ export interface Config {
19
26
  toolName?: string;
20
27
  /** Rendered-result ceiling, in characters: a longer JSON value is truncated with a notice (default 50000). */
21
28
  maxResultChars?: number;
29
+ /**
30
+ * Expose `run_in_background` (default true); disabled calls are also
31
+ * rejected. A background run needs a live `ctx.jobs` registry with a
32
+ * controller serving the caller (`dsh-jobs-local` plus `dsh-tool-jobs` in
33
+ * the shipped composition); without one the call fails with the missing
34
+ * piece named.
35
+ */
36
+ enableRunInBackground?: boolean;
22
37
  }
23
38
  export declare const Config: z<Config>;
24
39
  export declare function apply(ctx: Context, config: Config): void;
@@ -3,19 +3,23 @@
3
3
  * subagents, and return the script's final value. It owns the model-facing schema and run lifecycle; script
4
4
  * parsing, execution, caps, and cancellation live behind `ctx.workflowEngine`
5
5
  * (`@deepseek-ai/dsh-workflow`), so a hardened engine swaps in without touching what the model
6
- * sees. Execution awaits `run.result` and always disposes the run; non-completed reasons become tool
7
- * errors, and background collection remains deferred. Presentation is an args-only generic card
6
+ * sees. Foreground execution awaits `run.result` and always disposes the run; non-completed reasons
7
+ * become tool errors. `run_in_background: true` instead registers the run as an owned `ctx.jobs` job
8
+ * and returns its id immediately — the job's output ring streams live progress, and the run's value
9
+ * arrives with the job's completion notice. Presentation is an args-only generic card
8
10
  * titled from `meta.name`. Explicit-ask usage guidance is registered as the tool's own prompt
9
11
  * section rather than deployment persona prose.
10
12
  * @module @deepseek-ai/dsh-tool-workflow
11
13
  */
12
14
  import z from '@deepseek-ai/schemastery';
13
15
  import { defineTool } from '@deepseek-ai/dsh-tools';
16
+ import { createWorkflowRecordMirror } from "./record.js";
14
17
  export const name = 'tool-workflow';
15
18
  export const inject = ['tools', 'workflowEngine', 'systemPrompt'];
16
19
  export const Config = z.object({
17
20
  toolName: z.string().default('workflow'),
18
21
  maxResultChars: z.natural().min(1).default(50_000),
22
+ enableRunInBackground: z.boolean().default(true),
19
23
  });
20
24
  /** Render a contained recording failure without trusting the thrown value. */
21
25
  function renderRecordingError(error) {
@@ -89,7 +93,8 @@ function createWorkflowRecorder(ctx) {
89
93
  /**
90
94
  * The script-authoring contract, embedded in the tool description. This IS the
91
95
  * model-facing spec: the meta block, the hooks and their exact semantics, and
92
- * the supported schema subset.
96
+ * the supported schema subset. The closing execution sentence follows the
97
+ * composition: only a background-enabled tool describes `run_in_background`.
93
98
  */
94
99
  const DESCRIPTION = `Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.
95
100
 
@@ -103,7 +108,9 @@ Script-body hooks:
103
108
 
104
109
  Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps) throw errors that ALWAYS kill the script — they never dissolve into a per-item \`null\`.
105
110
 
106
- Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.`;
111
+ Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them.`;
112
+ const FOREGROUND_ONLY_CLOSING = ' The run executes in the foreground: this call returns when the whole script finishes.';
113
+ const BACKGROUND_CLOSING = ' The run executes in the foreground by default: this call returns when the whole script finishes. Set `run_in_background: true` for a long run: the call returns a job id immediately, the run keeps orchestrating in the background, and its return value arrives with the job\'s completion notice (check on it with `job_output`, stop it with `job_kill`).';
107
114
  /** The pending-state card: a generic card titled by the workflow's meta name. */
108
115
  function presentWorkflowCall(args) {
109
116
  return {
@@ -133,6 +140,31 @@ function stopReasonError(result) {
133
140
  /* v8 ignore stop */
134
141
  }
135
142
  }
143
+ /**
144
+ * Map a settled background run onto the job outcome vocabulary. A completed
145
+ * run carries the rendered return value as the job's result; a
146
+ * cancelled run leaves the detail to the registry's kill-reason merge (the
147
+ * cancel reason it forwarded is the same string); an errored run fails with
148
+ * the script's failure message.
149
+ */
150
+ function jobOutcomeOf(result, name, maxChars) {
151
+ switch (result.stopReason) {
152
+ case 'completed':
153
+ return {
154
+ status: 'completed',
155
+ detail: `${result.agentsStarted} agent${result.agentsStarted === 1 ? '' : 's'}`,
156
+ result: renderResult(name, result.agentsStarted, result.value, maxChars),
157
+ };
158
+ case 'cancelled':
159
+ return { status: 'killed' };
160
+ case 'error':
161
+ return { status: 'failed', detail: result.error ?? 'unknown error' };
162
+ /* v8 ignore start -- defensive: WorkflowStopReason is a closed union, exhaustive by construction; a future variant fails here loudly */
163
+ default:
164
+ return { status: 'failed', detail: `workflow run ended abnormally (${String(result.stopReason)})` };
165
+ /* v8 ignore stop */
166
+ }
167
+ }
136
168
  /** Render the run's outcome text: the meta name, agent count, and the JSON value (capped). */
137
169
  function renderResult(name, agentsStarted, value, maxChars) {
138
170
  // The engine returns JSON data (null for a valueless script), so stringify never yields undefined.
@@ -142,11 +174,74 @@ function renderResult(name, agentsStarted, value, maxChars) {
142
174
  : rendered;
143
175
  return `workflow "${name}" completed (${agentsStarted} agent${agentsStarted === 1 ? '' : 's'}).\nReturn value:\n${clipped}`;
144
176
  }
177
+ /**
178
+ * Register a background run as an owned job. The engine run is started
179
+ * inside the job starter with no tool-step signal — the run belongs to the
180
+ * job, so a registry kill or owner teardown is what cancels it — and its
181
+ * settlement is the job's settlement: dispose, stop the mirrors, then map the
182
+ * stop reason onto the job outcome (a completed run's rendered return value
183
+ * rides `result` to the model's first read after settlement).
184
+ * @param ctx - plugin context (engine, optional jobs registry, logger).
185
+ * @param args - the validated tool call.
186
+ * @param parent - the calling agent; owns the job.
187
+ * @param recordsRun - whether this top-level call records durable run events.
188
+ * @param deps - the tool's recorder/mirror taps and the render cap.
189
+ * @returns the background result for the tool's output schema.
190
+ */
191
+ function startBackgroundRun(ctx, args, parent, recordsRun, deps) {
192
+ const jobs = ctx.get('jobs');
193
+ if (jobs === undefined) {
194
+ throw new Error('background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs');
195
+ }
196
+ let run;
197
+ const jobId = jobs.start({
198
+ kind: 'workflow',
199
+ label: args.meta.name,
200
+ owner: parent.id,
201
+ run: (job) => {
202
+ // A synchronous engine rejection (META_INVALID/SCRIPT_PARSE) propagates
203
+ // out of the starter, so the registry registers nothing and the model
204
+ // sees the violation list as an ordinary tool error.
205
+ run = ctx.workflowEngine.start({
206
+ script: args.script,
207
+ meta: args.meta,
208
+ ...args.args !== undefined ? { args: args.args } : {},
209
+ parent,
210
+ });
211
+ deps.mirror.start(run.id, job);
212
+ if (recordsRun)
213
+ deps.recorder.start(parent.session, run);
214
+ const done = run.result.then(async (result) => {
215
+ try {
216
+ // Keep member listeners alive through disposal: an engine may
217
+ // synthesize cancelled member endings while reaching quiescence.
218
+ await run.dispose();
219
+ }
220
+ catch (error) {
221
+ // done must not reject; a failed disposal still has a settled result to report.
222
+ ctx.logger.warn(`background workflow run ${run.id} dispose failed: ${String(error)}`);
223
+ }
224
+ deps.mirror.stop(run.id);
225
+ if (recordsRun) {
226
+ deps.recorder.finish(run.id, result.stopReason);
227
+ deps.recorder.abandon(run.id);
228
+ }
229
+ return jobOutcomeOf(result, args.meta.name, deps.maxResultChars);
230
+ });
231
+ return {
232
+ cancel: (reason) => { run.cancel(reason ?? 'background workflow job killed'); },
233
+ done,
234
+ };
235
+ },
236
+ });
237
+ return { kind: 'background', jobId, runId: run.id };
238
+ }
145
239
  export function apply(ctx, config) {
146
240
  // schemastery (the exported Config schema) has already filled the defaulted
147
241
  // fields; the assertion records that resolution, not a hidden fallback.
148
- const { toolName, maxResultChars } = config;
242
+ const { toolName, maxResultChars, enableRunInBackground } = config;
149
243
  const recorder = createWorkflowRecorder(ctx);
244
+ const mirror = createWorkflowRecordMirror(ctx);
150
245
  // Usage policy ships with the tool (the master convention: tool guidance
151
246
  // lives in tool plugins as prompt sections, not in the deployment persona).
152
247
  ctx.systemPrompt.section({
@@ -156,7 +251,7 @@ export function apply(ctx, config) {
156
251
  });
157
252
  ctx.tools.register(defineTool({
158
253
  name: toolName,
159
- description: DESCRIPTION,
254
+ description: DESCRIPTION + (enableRunInBackground ? BACKGROUND_CLOSING : FOREGROUND_ONLY_CLOSING),
160
255
  parameters: {
161
256
  script: {
162
257
  type: 'string',
@@ -193,20 +288,42 @@ export function apply(ctx, config) {
193
288
  additionalProperties: true,
194
289
  description: 'Optional JSON input exposed to the script as the `args` global (wrap a bare list as a field, e.g. {"files": [...]}).',
195
290
  },
291
+ ...enableRunInBackground ? {
292
+ run_in_background: {
293
+ type: 'boolean',
294
+ description: 'Run as a background job: return a job id immediately instead of waiting; the return value arrives with the completion notice.',
295
+ },
296
+ } : {},
196
297
  },
197
298
  output: {
198
299
  schema: {
199
- type: 'object',
200
- additionalProperties: false,
201
- properties: {
202
- runId: { type: 'string', required: true },
203
- agentsStarted: { type: 'integer', required: true },
204
- result: { type: 'json', required: true },
205
- },
300
+ oneOf: [
301
+ {
302
+ type: 'object',
303
+ additionalProperties: false,
304
+ properties: {
305
+ kind: { type: 'string', required: true, const: 'background' },
306
+ jobId: { type: 'string', required: true },
307
+ runId: { type: 'string', required: true },
308
+ },
309
+ },
310
+ {
311
+ type: 'object',
312
+ additionalProperties: false,
313
+ properties: {
314
+ kind: { type: 'string', required: true, const: 'foreground' },
315
+ runId: { type: 'string', required: true },
316
+ agentsStarted: { type: 'integer', required: true },
317
+ result: { type: 'json', required: true },
318
+ },
319
+ },
320
+ ],
206
321
  },
207
322
  render: (args, value) => [{
208
323
  type: 'text',
209
- text: renderResult(args.meta.name, value.agentsStarted, value.result, maxResultChars),
324
+ text: value.kind === 'background'
325
+ ? `workflow "${args.meta.name}" started in the background as job ${value.jobId}. Its return value arrives with the completion notice; check on it with job_output, stop it with job_kill.`
326
+ : renderResult(args.meta.name, value.agentsStarted, value.result, maxResultChars),
210
327
  }],
211
328
  },
212
329
  async execute(args, exec) {
@@ -217,6 +334,21 @@ export function apply(ctx, config) {
217
334
  // parent to attribute the children to. Fail loud rather than guess.
218
335
  throw new Error('workflow tool requires a calling agent (exec.agent was undefined)');
219
336
  }
337
+ if (args.run_in_background === true) {
338
+ if (!enableRunInBackground) {
339
+ throw new Error('run_in_background is disabled for this tool');
340
+ }
341
+ // No pre-abort check here, unlike bash/pwsh: ToolRuntime re-reads the
342
+ // caller signal right before execute(), and this branch reaches
343
+ // jobs.start synchronously from there. The shell tools await a
344
+ // sandbox escalation approval before registering, which is the window
345
+ // their check covers.
346
+ return startBackgroundRun(ctx, args, parent, exec.parent === undefined, {
347
+ recorder,
348
+ mirror,
349
+ maxResultChars,
350
+ });
351
+ }
220
352
  // Meta/body validation failures (META_INVALID/SCRIPT_PARSE) throw
221
353
  // synchronously here and become isError results via the registry — the
222
354
  // model sees the violation list and can correct the call.
@@ -246,6 +378,7 @@ export function apply(ctx, config) {
246
378
  throw new Error(error);
247
379
  }
248
380
  return {
381
+ kind: 'foreground',
249
382
  runId: run.id,
250
383
  agentsStarted: result.agentsStarted,
251
384
  result: result.value,
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Live-progress mirror for background workflow runs: streams the engine's
3
+ * `workflow/phase`, `workflow/log`, and member lifecycle events into the
4
+ * owning job's output ring as `log` chunks — observer-only narration the
5
+ * model's `job_output` never renders — and keeps the job's live progress
6
+ * line on the current phase. Appends against a settled job log and drop
7
+ * inside the registry, so a straggling event after settlement is harmless.
8
+ * @module @deepseek-ai/dsh-tool-workflow/record
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import type { JobHandle } from '@deepseek-ai/dsh-jobs';
12
+ import type { WorkflowRunId } from '@deepseek-ai/dsh-workflow';
13
+ /** Job-ring taps for the background runs the tool tracks. */
14
+ export interface WorkflowRecordMirror {
15
+ /**
16
+ * Route a run's progress events into a job's ring. Call once per background
17
+ * run, from the job starter, before the worker publishes its first event.
18
+ * @param runId - the started run.
19
+ * @param job - the owning job's producer face.
20
+ */
21
+ start(runId: WorkflowRunId, job: JobHandle): void;
22
+ /**
23
+ * Stop routing a settled or abandoned run. Idempotent.
24
+ * @param runId - the run to drop.
25
+ */
26
+ stop(runId: WorkflowRunId): void;
27
+ }
28
+ /**
29
+ * Create the run-to-ring mirror and subscribe the engine's live progress
30
+ * events for the runs it tracks.
31
+ * @param ctx - plugin context whose event bus carries the `workflow/*` events.
32
+ * @returns the mirror taps the tool wires around each background run.
33
+ */
34
+ export declare function createWorkflowRecordMirror(ctx: Context): WorkflowRecordMirror;
35
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Live-progress mirror for background workflow runs: streams the engine's
3
+ * `workflow/phase`, `workflow/log`, and member lifecycle events into the
4
+ * owning job's output ring as `log` chunks — observer-only narration the
5
+ * model's `job_output` never renders — and keeps the job's live progress
6
+ * line on the current phase. Appends against a settled job log and drop
7
+ * inside the registry, so a straggling event after settlement is harmless.
8
+ * @module @deepseek-ai/dsh-tool-workflow/record
9
+ */
10
+ /**
11
+ * Create the run-to-ring mirror and subscribe the engine's live progress
12
+ * events for the runs it tracks.
13
+ * @param ctx - plugin context whose event bus carries the `workflow/*` events.
14
+ * @returns the mirror taps the tool wires around each background run.
15
+ */
16
+ export function createWorkflowRecordMirror(ctx) {
17
+ const active = new Map();
18
+ ctx.on('workflow/phase', (info, title) => {
19
+ const job = active.get(info.id);
20
+ if (job === undefined)
21
+ return;
22
+ job.updateProgress(title);
23
+ job.append(`▸ ${title}\n`, { channel: 'log' });
24
+ });
25
+ ctx.on('workflow/log', (info, message) => {
26
+ active.get(info.id)?.append(`${message}\n`, { channel: 'log' });
27
+ });
28
+ ctx.on('workflow/agent-start', (info, agent) => {
29
+ active.get(info.id)?.append(`agent #${agent.seq} ${agent.label} started\n`, { channel: 'log' });
30
+ });
31
+ ctx.on('workflow/agent-end', (info, agent) => {
32
+ active.get(info.id)?.append(`agent #${agent.seq} ${agent.outcome}\n`, { channel: 'log' });
33
+ });
34
+ return {
35
+ start(runId, job) {
36
+ active.set(runId, job);
37
+ },
38
+ stop(runId) {
39
+ active.delete(runId);
40
+ },
41
+ };
42
+ }
43
+ //# sourceMappingURL=record.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-tool-workflow",
3
3
  "description": "Model-facing workflow tool: run a JavaScript orchestration script over ctx.workflowEngine",
4
- "version": "0.1.6-alpha.2",
4
+ "version": "0.1.7-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,29 +37,34 @@
37
37
  ],
38
38
  "license": "MIT",
39
39
  "peerDependencies": {
40
- "@deepseek-ai/dsh-agent": "^0.1.6-alpha.2",
41
- "@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
42
- "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
43
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
44
- "@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
45
- "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
46
- "@deepseek-ai/dsh-workflow": "^0.1.6-alpha.2",
47
- "@deepseek-ai/cordis": "^4.0.2"
40
+ "@deepseek-ai/dsh-jobs": "0.1.7-alpha.2",
41
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
42
+ "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
43
+ "@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
44
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.2",
45
+ "@deepseek-ai/cordis": "~4.0.4",
46
+ "@deepseek-ai/dsh-tools": "0.1.7-alpha.2",
47
+ "@deepseek-ai/dsh-workflow": "0.1.7-alpha.2",
48
+ "@deepseek-ai/dsh-agent": "0.1.7-alpha.2"
48
49
  },
49
50
  "dependencies": {
50
- "@deepseek-ai/schemastery": "^3.18.2"
51
+ "@deepseek-ai/schemastery": "~3.18.4"
51
52
  },
52
53
  "devDependencies": {
53
- "@deepseek-ai/dsh-agent": "^0.1.6-alpha.2",
54
- "@deepseek-ai/dsh-invariants": "^0.1.6-alpha.2",
55
- "@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
56
- "@deepseek-ai/dsh-session": "^0.1.6-alpha.2",
57
- "@deepseek-ai/dsh-subagent": "^0.1.6-alpha.2",
58
- "@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
59
- "@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
60
- "@deepseek-ai/dsh-workflow-ptc": "^0.1.6-alpha.2",
61
- "@deepseek-ai/cordis": "^4.0.2",
62
- "@deepseek-ai/dsh-session-projection": "^0.1.6-alpha.2",
63
- "@deepseek-ai/dsh-workflow": "^0.1.6-alpha.2"
54
+ "@deepseek-ai/cordis": "~4.0.4",
55
+ "@deepseek-ai/dsh-agent": "0.1.7-alpha.2",
56
+ "@deepseek-ai/dsh-agent-loop-testkit": "0.1.7-alpha.2",
57
+ "@deepseek-ai/dsh-invariants": "0.1.7-alpha.2",
58
+ "@deepseek-ai/dsh-jobs": "0.1.7-alpha.2",
59
+ "@deepseek-ai/dsh-jobs-local": "0.1.7-alpha.2",
60
+ "@deepseek-ai/dsh-llm": "0.1.7-alpha.2",
61
+ "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.2",
62
+ "@deepseek-ai/dsh-session": "0.1.7-alpha.2",
63
+ "@deepseek-ai/dsh-subagent": "0.1.7-alpha.2",
64
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.2",
65
+ "@deepseek-ai/dsh-tool-jobs": "0.1.7-alpha.2",
66
+ "@deepseek-ai/dsh-tools": "0.1.7-alpha.2",
67
+ "@deepseek-ai/dsh-workflow-ptc": "0.1.7-alpha.2",
68
+ "@deepseek-ai/dsh-workflow": "0.1.7-alpha.2"
64
69
  }
65
70
  }