@deepseek-ai/dsh-tool-workflow 0.1.6-alpha.2 → 0.1.7-alpha.1
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 +2 -2
- package/README.md +21 -8
- package/README.zh.md +21 -8
- package/lib/index.js +179 -11
- package/lib/types/index.d.ts +17 -2
- package/lib/types/index.js +147 -14
- package/lib/types/record.d.ts +35 -0
- package/lib/types/record.js +43 -0
- package/package.json +26 -21
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:
|
|
6
|
-
README.zh.md:
|
|
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
|
|
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),
|
|
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
|
-
|
|
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.
|
|
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
|
-
- **
|
|
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:
|
|
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
|
|
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
|
-
|
|
32
|
+
模型提交三个参数外加一个开关:`meta`(必需的身份数据:`name`、`description`,以及可选的 `whenToUse` 与 `phases`)、`script`(必需的纯 JavaScript 脚本体——不含 `export const meta` 语句;工具描述携带完整的编写约定)、`args`(可选 JSON 对象,作为全局变量 `args` 向脚本公开;裸列表应包装到字段中,使协议 schema 如实表达形态),以及 `run_in_background`(可选;仅在 `enableRunInBackground` 生效时存在)。
|
|
33
33
|
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
10
|
-
* errors
|
|
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
|
|
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
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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.
|
|
7
|
-
* errors
|
|
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;
|
package/lib/types/index.js
CHANGED
|
@@ -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.
|
|
7
|
-
* errors
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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:
|
|
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.
|
|
4
|
+
"version": "0.1.7-alpha.1",
|
|
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/
|
|
41
|
-
"@deepseek-ai/dsh-
|
|
42
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
45
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
46
|
-
"@deepseek-ai/dsh-workflow": "^0.1.
|
|
47
|
-
"@deepseek-ai/
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.3",
|
|
41
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
|
|
42
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
|
|
43
|
+
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
|
|
44
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
|
|
45
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
|
|
46
|
+
"@deepseek-ai/dsh-workflow": "^0.1.7-alpha.1",
|
|
47
|
+
"@deepseek-ai/dsh-jobs": "^0.1.7-alpha.1",
|
|
48
|
+
"@deepseek-ai/dsh-invariants": "^0.1.7-alpha.1"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
51
|
+
"@deepseek-ai/schemastery": "^3.18.3"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@deepseek-ai/
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
54
|
+
"@deepseek-ai/cordis": "^4.0.3",
|
|
55
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
|
|
56
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.7-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-invariants": "^0.1.7-alpha.1",
|
|
58
|
+
"@deepseek-ai/dsh-jobs": "^0.1.7-alpha.1",
|
|
59
|
+
"@deepseek-ai/dsh-jobs-local": "^0.1.7-alpha.1",
|
|
60
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
|
|
61
|
+
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
|
|
62
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.7-alpha.1",
|
|
63
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.7-alpha.1",
|
|
64
|
+
"@deepseek-ai/dsh-tool-jobs": "^0.1.7-alpha.1",
|
|
65
|
+
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
|
|
66
|
+
"@deepseek-ai/dsh-workflow": "^0.1.7-alpha.1",
|
|
67
|
+
"@deepseek-ai/dsh-workflow-ptc": "^0.1.7-alpha.1",
|
|
68
|
+
"@deepseek-ai/dsh-subagent": "^0.1.7-alpha.1"
|
|
64
69
|
}
|
|
65
70
|
}
|