@x1a0f3n9/dsh-tool-jobs 0.1.5-rc.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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/lib/index.js +353 -0
- package/lib/types/index.d.ts +54 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/jobs/tool-jobs/README.md
|
|
5
|
+
README.md: 8f3de4276d766f931f9461d25d15bf33aec26856
|
|
6
|
+
README.zh.md: 83f8fca0cd7e74f4c4687351752bd8c36b002647
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The model-facing background-job controls for users and maintainers choosing, configuring, or debugging job_output, job_list, job_kill, and completion notices."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @x1a0f3n9/dsh-tool-jobs
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Use `dsh-tool-jobs` to inspect and control background commands, PTY work, and subagents through `job_output`, `job_list`, and `job_kill`. Reads can wait within a configured timeout, list results identify each job's kind and status, and cancellation settles only after the work stops. When owned work finishes, the agent receives an in-session notice: busy agents receive it in their next step, while idle agents may be woken by a bounded follow-up turn. Configuration controls wait limits, completion delivery, and consecutive wakeups. Stream output is consumed by one reader, and pending notices do not survive owner disposal.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Load this plugin in any composition where the agent should start, observe, and stop background jobs: it registers the three tools, attaches the controller producers need, and delivers completion notices. It requires the `ctx.tools`, `ctx.jobs`, and `ctx.systemPrompt` services from the composed harness.
|
|
29
|
+
|
|
30
|
+
### The three tools
|
|
31
|
+
|
|
32
|
+
- `job_output(job_id, wait?, timeout_ms?)` — Read a job's output. Stream jobs return only the output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap and leaves a still-running job alive on timeout.
|
|
33
|
+
- `job_list()` — List your background jobs with their ids, kinds, and statuses, one per line: `<id> [<kind>] <status> — <label>`.
|
|
34
|
+
- `job_kill(job_id, reason?)` — Request cancellation of a running job immediately; the job settles as `killed` once its work actually stops. A terminal job returns its current snapshot, and the optional reason is recorded and forwarded to the job.
|
|
35
|
+
|
|
36
|
+
The three tools return `{ text, job }`, `PublicJobSnapshot[]`, and `{ outcome: 'cancellation-requested' | 'already-finished', job }` respectively. A public snapshot carries id, kind, label, status/detail, and start/finish times and omits ownership and notification bookkeeping. All three render through generic UI cards: `read` for output and list, `execute` for kill.
|
|
37
|
+
|
|
38
|
+
### Completion notices
|
|
39
|
+
|
|
40
|
+
When a job finishes, the owning agent receives `background job <id> (<kind>: <label>) finished [status: ...]. Read its output with job_output.` as an in-session message. A busy agent has the notice injected into its next step — the turn cannot close while the inbox holds it, so several jobs settling together cost one step rather than one turn each. An idle agent is instead woken with a follow-up turn, because an unclaimed notice is a completion the model never learns about. A kill or a terminal read/wait marks the completion reported and suppresses the redundant notice, as does the teardown cancel that drains an owner or the service.
|
|
41
|
+
|
|
42
|
+
Waking is bounded: each owner may be woken `maxConsecutiveWakes` times before further notices degrade to injection, and claiming any user-authored message restores the budget. The bound exists because the chain is self-exciting — a woken turn may start the background job whose completion wakes it again. `completionDelivery: quiet` keeps even idle owners on the injection lane, which deterministic transcripts need.
|
|
43
|
+
|
|
44
|
+
### Minimal configuration
|
|
45
|
+
|
|
46
|
+
Loading the plugin with no config is the common path; a `waitTimeoutMs` above `maxWaitTimeoutMs` fails at load.
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- name: '@x1a0f3n9/dsh-tool-jobs'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Field | Default | Meaning |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `waitTimeoutMs` | `30,000` | Wait used when `wait: true` omits `timeout_ms` |
|
|
55
|
+
| `maxWaitTimeoutMs` | `600,000` | Cap for model-supplied waits; larger values clamp down to it |
|
|
56
|
+
| `completionDelivery` | `wakeup` | `wakeup` opens a turn on an idle owner; `quiet` leaves the notice pending |
|
|
57
|
+
| `maxConsecutiveWakes` | `3` | Turns one owner may open by wake before notices degrade to injection |
|
|
58
|
+
|
|
59
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-jobs) is the exhaustive source for every accepted field and its JSDoc.
|
|
60
|
+
|
|
61
|
+
### What can go wrong
|
|
62
|
+
|
|
63
|
+
An agent whose composition loads no `tool-jobs` cannot start background work: this plugin's controller is what arms producers' `ctx.jobs.start()`. A model-supplied wait longer than `maxWaitTimeoutMs` is clamped down to the cap, and a timed-out wait returns `[status: running]` and leaves the job alive rather than failing. A completion notice pending on an idle owner does not survive that owner's disposal.
|
|
64
|
+
|
|
65
|
+
-----
|
|
66
|
+
|
|
67
|
+
<a id="understand-the-implementation"></a>
|
|
68
|
+
## Understand the implementation
|
|
69
|
+
|
|
70
|
+
<details>
|
|
71
|
+
<summary>Implementation internals — click to expand</summary>
|
|
72
|
+
|
|
73
|
+
This section explains the design decisions behind the tools and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
74
|
+
|
|
75
|
+
### Design philosophy
|
|
76
|
+
|
|
77
|
+
- **Kind-independent controls.** The same three tools read, list, and cancel jobs of every producer kind — bash, subagent, PTY — because all of them register through the generic `ctx.jobs` runtime.
|
|
78
|
+
- **Delivery is owned here; recipients are the registry's.** The plugin decides how an unreported completion reaches the owner — injected into a busy step, or a woken turn on an idle owner — while the registry routes each settlement to the listeners its owner's scope chain reaches, so a mount under one preset never sees another preset's agents, and an agent reads exactly one notice per completion however many presets are mounted.
|
|
79
|
+
- **Producer-owned output bounds.** When a producer supplies `outputLimitBytes`, the complete model-facing result — output read, terminal kill snapshot, or completion notice — is capped after status and notice metadata are added; producers that omit it keep unbounded behavior.
|
|
80
|
+
|
|
81
|
+
### Source map
|
|
82
|
+
|
|
83
|
+
| File | Role |
|
|
84
|
+
|---|---|
|
|
85
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: tool registrations, completion listener, prompt section, output capping |
|
|
86
|
+
| — | No runtime invariant companion is published; this model-facing adapter has no independent lifecycle stream; execution relations are owned by the capability seam it calls. |
|
|
87
|
+
|
|
88
|
+
### Output capping
|
|
89
|
+
|
|
90
|
+
`job_output` and `job_kill` capture the caller-visible job in a prepended pre-execute listener before policy runs, so the producer's cap applies to the complete rendered result. `job_output` preserves its output/status split when policy left the default rendering intact, bounding the output tail and the `[status: ...]` suffix; other single-text results — denials, short-circuits, normalized tool or pipeline failures, replacements, and blocks — are bounded as one text, while structured multi-block policy results keep their shape. A bounded completion notice reserves the stable `background job <id>` prefix and the `job_output` collection instruction before spending remaining bytes on variable kind, label, status, detail, and truncation marker, so the notice stays actionable at the PTY's supported 64-byte minimum; an existing producer truncation marker is reused rather than duplicated.
|
|
91
|
+
|
|
92
|
+
### Notice delivery lanes
|
|
93
|
+
|
|
94
|
+
`onJobDone` skips jobs already reported or unowned. A `wakeup` delivery opens a turn on an idle owner while the budget lasts, tracked per exact `Agent` in a `WeakMap`; claiming a user-authored message (`agent/inbox/claimed`) resets that owner's budget. A busy owner — or any notice past the budget, or `quiet` delivery — is injected into the next-step inbox instead. Teardown settlements arrive already `reported`, so disposal never spends a model request announcing a notice nobody can read.
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
<a id="further-exploration"></a>
|
|
101
|
+
## Further Exploration
|
|
102
|
+
|
|
103
|
+
Read these pages when the package-level contract is not enough. They move from the job types to the registry contract and the generated schemas.
|
|
104
|
+
|
|
105
|
+
- [Background task runtime subsystem](../../../docs/subsystems/jobs.md) — the job types, snapshot fields, and `ctx.jobs` Cordis surface.
|
|
106
|
+
- [jobs group map](../README.md) — the sibling group page and its package table.
|
|
107
|
+
- [Registry contract](../jobs/README.md) — the abstract `ctx.jobs` service behind the tools.
|
|
108
|
+
- [Process-local registry](../jobs-local/README.md) — where jobs run in this process.
|
|
109
|
+
- [Generated tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-jobs) — the exact `job_output`, `job_list`, and `job_kill` schemas.
|
|
110
|
+
- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-jobs) — every accepted config field and its source declaration.
|
|
111
|
+
- [job-registry seam Agent Note](../../../.agents/notes/archived/architecture/2026-07-26-job-registry-seam.md) — the owner-fenced registry contract and its rationale.
|
|
112
|
+
|
|
113
|
+
-----
|
|
114
|
+
|
|
115
|
+
<a id="model-experience"></a>
|
|
116
|
+
## Model Experience
|
|
117
|
+
|
|
118
|
+
### System prompt
|
|
119
|
+
|
|
120
|
+
#### What the model sees
|
|
121
|
+
|
|
122
|
+
Every request in this plugin's registration scope contains this guidance. Agent-scoped tool filtering may hide the tools without removing the independently registered prompt section.
|
|
123
|
+
|
|
124
|
+
##### Background-job guidance
|
|
125
|
+
|
|
126
|
+
```markdown
|
|
127
|
+
Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Token effect
|
|
131
|
+
|
|
132
|
+
Small fixed input cost per request while active.
|
|
133
|
+
|
|
134
|
+
#### KV Cache effect
|
|
135
|
+
|
|
136
|
+
Prefix-stable while the plugin scope and guidance text are unchanged. Activation or disposal may invalidate reuse from this prompt section.
|
|
137
|
+
|
|
138
|
+
### Tool schemas
|
|
139
|
+
|
|
140
|
+
#### What the model sees
|
|
141
|
+
|
|
142
|
+
The generated [`job_output`, `job_list`, and `job_kill` schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-jobs) while this tool set is visible.
|
|
143
|
+
|
|
144
|
+
#### Token effect
|
|
145
|
+
|
|
146
|
+
Fixed schema cost on each request where the tools are visible.
|
|
147
|
+
|
|
148
|
+
#### KV Cache effect
|
|
149
|
+
|
|
150
|
+
Prefix-stable while tool definitions and visibility are unchanged. Registration lifecycle or scoped restrictions may invalidate reuse from the first changed schema token.
|
|
151
|
+
|
|
152
|
+
### Results and notices
|
|
153
|
+
|
|
154
|
+
#### What the model sees
|
|
155
|
+
|
|
156
|
+
Reads return output or `(no new output)` followed by `[status: <status>]` and optional detail. An empty list returns `(no background jobs)`. Kill returns `requested cancellation of job <id>` or the existing terminal status. An unreported owned completion uses the notice above.
|
|
157
|
+
|
|
158
|
+
#### Token effect
|
|
159
|
+
|
|
160
|
+
Results and notices remain in parent history until compaction. Stream reads do not repeat consumed output; a producer-supplied `outputLimitBytes` bounds each complete read or notice. Under `wakeup`, a notice reaching an idle owner also buys a model request the user did not ask for, capped per owner by `maxConsecutiveWakes`; a notice reaching a busy owner adds a step to the turn it is already paying for.
|
|
161
|
+
|
|
162
|
+
#### KV Cache effect
|
|
163
|
+
|
|
164
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV Cache entries.
|
|
165
|
+
|
|
166
|
+
## Known Limitations and Deferred Work
|
|
167
|
+
|
|
168
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
These limits define when the tools are a poor fit. They are current package constraints, not a task backlog.
|
|
172
|
+
|
|
173
|
+
- **A settlement inside the driver's retirement window still strands its notice** — between the turn loop's last inbox check and the driver committing its idle phase the owner still reads as busy, so the notice is injected and nothing wakes. Steering has the same hole; closing it belongs to `agent-loop`.
|
|
174
|
+
- **A spent wake budget is not restored by time** — only user-authored input refills it, so an unattended agent whose budget ran out collects its remaining notices on the next turn something else opens.
|
|
175
|
+
- **A notice pending on an idle owner does not survive that owner's disposal** — the disposal cancel clears the unclaimed inbox, and the log keeps the insert/cancel pair as the record.
|
|
176
|
+
- **Stream reads are single-consumer** — independent observers need another runtime API.
|
|
177
|
+
- **Unowned jobs have no session fence** — external callers must supply policy or avoid them.
|
|
178
|
+
|
|
179
|
+
<a id="dev-note"></a>
|
|
180
|
+
### Dev Note
|
|
181
|
+
|
|
182
|
+
<details>
|
|
183
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
184
|
+
|
|
185
|
+
None.
|
|
186
|
+
|
|
187
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向模型的后台任务控制,供选择、配置或排查 job_output、job_list、job_kill 与完成通知的用户与维护者阅读。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @x1a0f3n9/dsh-tool-jobs
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
使用 `dsh-tool-jobs`,可通过 `job_output`、`job_list` 与 `job_kill` 检查和控制后台命令、PTY 工作与 subagent。读取可在配置的超时内等待,列表结果标识各任务的 kind 与状态,而取消只有在工作停止后才结算。归属明确的工作完成时,agent(智能体)会收到会话内通知:繁忙的 agent 在下一步收到通知,空闲的 agent 则可能由有界的 follow-up 轮次唤醒。配置控制等待上限、完成投递与连续唤醒次数。流输出仅供单一读取方消费,待领通知无法在所有者释放后存活。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
在 agent 需要启动、观察和停止后台任务的任何组合中加载本插件:它注册三个工具、附加生产方所需的控制器,并投递完成通知。它需要组合中已提供的 `ctx.tools`、`ctx.jobs` 与 `ctx.systemPrompt` 服务。
|
|
29
|
+
|
|
30
|
+
### 三个工具
|
|
31
|
+
|
|
32
|
+
- `job_output(job_id, wait?, timeout_ms?)`——读取任务输出。流任务只返回自上次读取以来的输出;最终输出任务在结算后返回其结果。每个响应都以 `[status: ...]` 结尾。除非 `wait: true`,否则读取是非阻塞的;`wait: true` 最多等待到配置上限,超时时仍让运行中的任务保持存活。
|
|
33
|
+
- `job_list()`——列出你的后台任务及其 id、kind 与状态,每行一个:`<id> [<kind>] <status> — <label>`。
|
|
34
|
+
- `job_kill(job_id, reason?)`——立即请求取消运行中的任务;任务在其工作真正停止后以 `killed` 结算。终止任务返回其当前快照,可选的原因会被记录并转发给任务。
|
|
35
|
+
|
|
36
|
+
三个工具依次返回 `{ text, job }`、`PublicJobSnapshot[]` 与 `{ outcome: 'cancellation-requested' | 'already-finished', job }`。公共快照携带 id、kind、label、status/detail 及开始/结束时间,并省略归属与通知簿记字段。三个工具都通过通用 UI 卡片渲染:output 和 list 用 `read`,kill 用 `execute`。
|
|
37
|
+
|
|
38
|
+
### 完成通知
|
|
39
|
+
|
|
40
|
+
任务完成时,拥有它的 agent 会收到会话内消息 `background job <id> (<kind>: <label>) finished [status: ...]. Read its output with job_output.`。繁忙的 agent 会在下一步收到注入的通知——inbox 尚有内容时轮次无法结束,因此同时结算的多个任务只花掉一步,而不是各占一轮。空闲的 agent 则被一个 follow-up 轮次唤醒,因为无人领取的通知等于模型永远不会知道的完成。kill 或针对终止任务的 read/wait 会把完成标为已报告并抑制重复通知;排空 owner 或服务的 teardown 取消同样如此。
|
|
41
|
+
|
|
42
|
+
唤醒是有界的:每个所有者最多可被唤醒 `maxConsecutiveWakes` 次,此后的通知降级为注入;领取任何用户撰写的消息都会恢复预算。设界是因为这条链会自激——被唤醒的一轮可能启动某个后台任务,而它的完成又会唤醒同一个所有者。`completionDelivery: quiet` 让空闲所有者也在注入通道上,确定性 transcript(文本记录)需要的正是这一点。
|
|
43
|
+
|
|
44
|
+
### 最小配置
|
|
45
|
+
|
|
46
|
+
不带配置加载插件是常用路径;`waitTimeoutMs` 高于 `maxWaitTimeoutMs` 时会在加载时失败。
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
- name: '@x1a0f3n9/dsh-tool-jobs'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| 字段 | 默认值 | 含义 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `waitTimeoutMs` | `30,000` | `wait: true` 省略 `timeout_ms` 时使用的等待时间 |
|
|
55
|
+
| `maxWaitTimeoutMs` | `600,000` | 模型所给等待时间的上限;更大的值向下收敛到它 |
|
|
56
|
+
| `completionDelivery` | `wakeup` | `wakeup` 为空闲所有者开启一轮;`quiet` 让通知继续待领 |
|
|
57
|
+
| `maxConsecutiveWakes` | `3` | 一个所有者可由唤醒开启的轮数,超出后通知降级为注入 |
|
|
58
|
+
|
|
59
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-jobs)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
|
60
|
+
|
|
61
|
+
### 可能出什么问题
|
|
62
|
+
|
|
63
|
+
组合中未加载 `tool-jobs` 的 agent 无法启动后台工作:本插件的控制器正是生产方 `ctx.jobs.start()` 得以启用所依赖的。模型给出的等待时间超过 `maxWaitTimeoutMs` 时会向下收敛到上限,超时的等待返回 `[status: running]` 并让任务保持存活,而不是失败。待领于空闲所有者的完成通知无法在该所有者释放后存活。
|
|
64
|
+
|
|
65
|
+
-----
|
|
66
|
+
|
|
67
|
+
<a id="understand-the-implementation"></a>
|
|
68
|
+
## 理解实现
|
|
69
|
+
|
|
70
|
+
<details>
|
|
71
|
+
<summary>实现细节——点击展开</summary>
|
|
72
|
+
|
|
73
|
+
本节解释工具背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
74
|
+
|
|
75
|
+
### 设计理念
|
|
76
|
+
|
|
77
|
+
- **与 kind 无关的控制。** 同一套三个工具读取、列出和取消每种生产方 kind 的任务——bash、subagent、PTY——因为它们都通过通用的 `ctx.jobs` 运行时注册。
|
|
78
|
+
- **投递归本插件,收件人归注册表。** 插件决定未报告的完成如何到达所有者——注入繁忙的一步,或唤醒空闲所有者的一轮——而注册表把每次结算路由给其所有者 scope 链所能抵达的监听器,因此某个 preset 下的挂载永远看不到另一个 preset 的 agent,无论挂载了多少 preset,一个 agent 每次完成都只读到一条通知。
|
|
79
|
+
- **生产方自有的输出上限。** 生产方提供 `outputLimitBytes` 时,完整的模型侧结果——输出读取、终止 kill 快照或完成通知——会在添加状态与通知元数据之后被施加上限;省略该字段的生产方保持无界行为。
|
|
80
|
+
|
|
81
|
+
### 源码地图
|
|
82
|
+
|
|
83
|
+
| 文件 | 职责 |
|
|
84
|
+
|---|---|
|
|
85
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:工具注册、完成监听器、提示词区段、输出上限 |
|
|
86
|
+
| — | 不发布运行时不变式伴生入口;这个面向模型的适配器没有独立的生命周期流;执行关系归其调用的能力 seam 所有。 |
|
|
87
|
+
|
|
88
|
+
### 输出上限
|
|
89
|
+
|
|
90
|
+
`job_output` 与 `job_kill` 会在策略运行前于前置的 pre-execute 监听器中捕获调用方可见任务,因此生产方上限适用于完整渲染结果。`job_output` 在策略保留默认渲染时保持其输出/状态拆分,对输出尾部与 `[status: ...]` 后缀设界;其他单文本结果——拒绝、短路、规范化工具或流水线失败、替换与阻止——按单个文本设界,而结构化多块策略结果保持自身形状。有界完成通知先为稳定的 `background job <id>` 前缀与 `job_output` 收集指令预留空间,再把剩余字节用于可变的 kind、label、status、detail 与截断标记,因此在 PTY 支持的 64 字节下限下通知仍可操作;已有的生产方截断标记会被复用,不会重复添加。
|
|
91
|
+
|
|
92
|
+
### 通知投递通道
|
|
93
|
+
|
|
94
|
+
`onJobDone` 跳过已报告或无所有者的任务。`wakeup` 投递在预算内为空闲所有者开启一轮,按确切 `Agent` 记录在 `WeakMap` 中;领取用户撰写的消息(`agent/inbox/claimed`)会重置该所有者的预算。繁忙的所有者——或超出预算的任何通知,以及 `quiet` 投递——改为注入 next-step inbox。teardown 结算抵达时已标记为 `reported`,因此释放永远不会花一次模型请求来宣布无人能读的通知。
|
|
95
|
+
|
|
96
|
+
</details>
|
|
97
|
+
|
|
98
|
+
-----
|
|
99
|
+
|
|
100
|
+
<a id="further-exploration"></a>
|
|
101
|
+
## 进一步探索
|
|
102
|
+
|
|
103
|
+
当包级约定不够用时阅读以下页面。它们从任务类型逐步进入注册表约定与生成 schema。
|
|
104
|
+
|
|
105
|
+
- [后台任务运行时子系统](../../../docs/subsystems/jobs.zh.md)——任务类型、快照字段与 `ctx.jobs` 的 Cordis 接口面。
|
|
106
|
+
- [jobs 组映射](../README.zh.md)——同级组页面及其包表格。
|
|
107
|
+
- [注册表约定](../jobs/README.zh.md)——工具背后的抽象 `ctx.jobs` 服务。
|
|
108
|
+
- [进程本地注册表](../jobs-local/README.zh.md)——任务在本进程中的运行位置。
|
|
109
|
+
- [生成的工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-jobs)——`job_output`、`job_list` 与 `job_kill` 的确切 schema。
|
|
110
|
+
- [生成的配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-jobs)——每个受支持配置字段及其源声明。
|
|
111
|
+
- [任务注册表 seam Agent Note](../../../.agents/notes/archived/architecture/2026-07-26-job-registry-seam.md)——按所有者隔离的注册表约定及其理由。
|
|
112
|
+
|
|
113
|
+
-----
|
|
114
|
+
|
|
115
|
+
<a id="model-experience"></a>
|
|
116
|
+
## 模型体验
|
|
117
|
+
|
|
118
|
+
### 系统提示词
|
|
119
|
+
|
|
120
|
+
#### 模型看到什么
|
|
121
|
+
|
|
122
|
+
该插件注册 scope 中的每次请求都包含以下指引。按 agent scope 过滤工具时,可能会隐藏工具,却不会移除独立注册的提示词区段。
|
|
123
|
+
|
|
124
|
+
##### 后台任务指引
|
|
125
|
+
|
|
126
|
+
```markdown
|
|
127
|
+
Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Token 影响
|
|
131
|
+
|
|
132
|
+
激活期间,每次请求都会产生少量固定的输入 token 开销。
|
|
133
|
+
|
|
134
|
+
#### KV Cache 影响
|
|
135
|
+
|
|
136
|
+
只要插件 scope 与指引文本不变,前缀就保持稳定。激活或释放可能使从该提示词区段起的复用失效。
|
|
137
|
+
|
|
138
|
+
### 工具 schema
|
|
139
|
+
|
|
140
|
+
#### 模型看到什么
|
|
141
|
+
|
|
142
|
+
该工具集可见时,会看到生成的 [`job_output`、`job_list` 和 `job_kill` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-jobs)。
|
|
143
|
+
|
|
144
|
+
#### Token 影响
|
|
145
|
+
|
|
146
|
+
工具可见时,每次请求都会产生固定的 schema token 开销。
|
|
147
|
+
|
|
148
|
+
#### KV Cache 影响
|
|
149
|
+
|
|
150
|
+
只要工具定义与可见性不变,前缀就保持稳定。注册生命周期或 scope 限制可能使从第一个发生变化的 schema token 起的复用失效。
|
|
151
|
+
|
|
152
|
+
### 结果与通知
|
|
153
|
+
|
|
154
|
+
#### 模型看到什么
|
|
155
|
+
|
|
156
|
+
读取会返回输出或 `(no new output)`,随后是 `[status: <status>]` 和可选 detail。空列表返回 `(no background jobs)`。kill 返回 `requested cancellation of job <id>` 或现有终止状态。尚未报告且有所有者归属的完成使用上述通知。
|
|
157
|
+
|
|
158
|
+
#### Token 影响
|
|
159
|
+
|
|
160
|
+
结果与通知在压缩(compaction)前保留于父级历史。流读取不会重复已消费的输出;生产方提供的 `outputLimitBytes` 会限制每次完整读取或通知。在 `wakeup` 下,抵达空闲所有者的通知还会额外买下一次用户并未要求的模型请求,其数量按所有者由 `maxConsecutiveWakes` 封顶;抵达繁忙所有者的通知则只是给它已经在支付的那一轮加一步。
|
|
161
|
+
|
|
162
|
+
#### KV Cache 影响
|
|
163
|
+
|
|
164
|
+
仅追加;新可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
165
|
+
|
|
166
|
+
## 已知限制与延期工作
|
|
167
|
+
|
|
168
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
这些限制说明工具何时不合适。它们是当前包约束,不是任务积压。
|
|
172
|
+
|
|
173
|
+
- **落在 driver 退休窗口内的结算仍会让通知搁浅**——在轮次循环最后一次检查 inbox 与 driver 提交 idle 相位之间,所有者读起来仍是繁忙,因此通知走注入且无人唤醒。steering(中途引导)存在同样的问题;修复它属于 `agent-loop`。
|
|
174
|
+
- **已花掉的唤醒预算不会随时间恢复**——只有用户撰写的输入才能补充,因此预算耗尽的无人值守 agent 要等到其他原因开启下一轮时才收走剩余通知。
|
|
175
|
+
- **待领于空闲所有者的通知无法在该所有者释放后存活**——释放时的取消会清空未领取的 inbox,日志保留插入/取消这一对作为记录。
|
|
176
|
+
- **流读取只有单一消费方**——独立观察者需要另一套运行时 API。
|
|
177
|
+
- **无 owner 的任务没有会话隔离**——外部调用方必须提供策略或避开这些任务。
|
|
178
|
+
|
|
179
|
+
<a id="dev-note"></a>
|
|
180
|
+
### 开发备注
|
|
181
|
+
|
|
182
|
+
<details>
|
|
183
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
184
|
+
|
|
185
|
+
无。
|
|
186
|
+
|
|
187
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { boundContextSummary, createUserMessage } from "@x1a0f3n9/dsh-llm";
|
|
3
|
+
import { TextRetainer } from "@x1a0f3n9/dsh-output-retention";
|
|
4
|
+
import { defineTool } from "@x1a0f3n9/dsh-tools";
|
|
5
|
+
import { JobId } from "@x1a0f3n9/dsh-jobs";
|
|
6
|
+
//#region lib/types/index.js
|
|
7
|
+
/**
|
|
8
|
+
* Model-facing `job_output`, `job_list`, and `job_kill` tools over
|
|
9
|
+
* `ctx.jobs`. Loading the plugin attaches the controller required by
|
|
10
|
+
* producers. It also delivers unreported completions to the owning agent:
|
|
11
|
+
* injected into a busy owner's next step, or opening a turn on an idle one
|
|
12
|
+
* under the default `wakeup` delivery, bounded per owner.
|
|
13
|
+
* @module @x1a0f3n9/dsh-tool-jobs
|
|
14
|
+
*/
|
|
15
|
+
const name = "tool-jobs";
|
|
16
|
+
const inject = [
|
|
17
|
+
"tools",
|
|
18
|
+
"jobs",
|
|
19
|
+
"systemPrompt"
|
|
20
|
+
];
|
|
21
|
+
const Config = z.object({
|
|
22
|
+
waitTimeoutMs: z.number().min(1).default(3e4),
|
|
23
|
+
maxWaitTimeoutMs: z.number().min(1).default(6e5),
|
|
24
|
+
completionDelivery: z.union(["quiet", "wakeup"]).default("wakeup"),
|
|
25
|
+
maxConsecutiveWakes: z.number().min(1).default(3)
|
|
26
|
+
});
|
|
27
|
+
/** Shared schema for job-control outputs. */
|
|
28
|
+
const PUBLIC_TASK_SCHEMA = {
|
|
29
|
+
type: "object",
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: {
|
|
32
|
+
id: {
|
|
33
|
+
type: "string",
|
|
34
|
+
required: true
|
|
35
|
+
},
|
|
36
|
+
kind: {
|
|
37
|
+
type: "string",
|
|
38
|
+
required: true
|
|
39
|
+
},
|
|
40
|
+
label: {
|
|
41
|
+
type: "string",
|
|
42
|
+
required: true
|
|
43
|
+
},
|
|
44
|
+
status: {
|
|
45
|
+
type: "string",
|
|
46
|
+
required: true,
|
|
47
|
+
enum: [
|
|
48
|
+
"running",
|
|
49
|
+
"stopping",
|
|
50
|
+
"completed",
|
|
51
|
+
"killed",
|
|
52
|
+
"failed"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
detail: { type: "string" },
|
|
56
|
+
startedAt: {
|
|
57
|
+
type: "integer",
|
|
58
|
+
required: true
|
|
59
|
+
},
|
|
60
|
+
finishedAt: { type: "integer" }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/** Remove job ownership and notification bookkeeping from a registry snapshot. */
|
|
64
|
+
function publicJob(snapshot) {
|
|
65
|
+
return {
|
|
66
|
+
id: snapshot.id,
|
|
67
|
+
kind: snapshot.kind,
|
|
68
|
+
label: snapshot.label,
|
|
69
|
+
status: snapshot.status,
|
|
70
|
+
...snapshot.detail !== void 0 ? { detail: snapshot.detail } : {},
|
|
71
|
+
startedAt: snapshot.startedAt,
|
|
72
|
+
...snapshot.finishedAt !== void 0 ? { finishedAt: snapshot.finishedAt } : {}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Render generic status with optional producer detail.
|
|
77
|
+
* @param snapshot - job state to render.
|
|
78
|
+
* @returns a bracketed status line.
|
|
79
|
+
*/
|
|
80
|
+
function statusLine(snapshot) {
|
|
81
|
+
return snapshot.detail !== void 0 ? `[status: ${snapshot.status}, ${snapshot.detail}]` : `[status: ${snapshot.status}]`;
|
|
82
|
+
}
|
|
83
|
+
const encoder = new TextEncoder();
|
|
84
|
+
function retainTail(text, maxBytes) {
|
|
85
|
+
const retainer = new TextRetainer({
|
|
86
|
+
kind: "tail",
|
|
87
|
+
maxBytes
|
|
88
|
+
});
|
|
89
|
+
retainer.push(text);
|
|
90
|
+
return retainer.finish().text;
|
|
91
|
+
}
|
|
92
|
+
function retainHead(text, maxBytes) {
|
|
93
|
+
const retainer = new TextRetainer({
|
|
94
|
+
kind: "head",
|
|
95
|
+
maxBytes
|
|
96
|
+
});
|
|
97
|
+
retainer.push(text);
|
|
98
|
+
return retainer.finish().text;
|
|
99
|
+
}
|
|
100
|
+
function fitWithSuffix(content, suffix, maxBytes, omitted) {
|
|
101
|
+
const complete = `${content}${suffix}`;
|
|
102
|
+
if (maxBytes === void 0 || encoder.encode(complete).byteLength <= maxBytes) return complete;
|
|
103
|
+
const fixed = `${content.endsWith(omitted.trimStart()) ? "" : omitted}${suffix}`;
|
|
104
|
+
const fixedBytes = encoder.encode(fixed).byteLength;
|
|
105
|
+
if (fixedBytes >= maxBytes) return retainTail(fixed, maxBytes);
|
|
106
|
+
return `${retainTail(content, maxBytes - fixedBytes)}${fixed}`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One-line account of a settled job for the `notice` form's collapsed row.
|
|
110
|
+
* @param snapshot - the settled job.
|
|
111
|
+
* @returns its kind, label, and status, bounded like every notice summary.
|
|
112
|
+
*/
|
|
113
|
+
function completionSummary(snapshot) {
|
|
114
|
+
return boundContextSummary(`${snapshot.kind} ${snapshot.label} ${statusLine(snapshot)}`);
|
|
115
|
+
}
|
|
116
|
+
function fitCompletionNotice(snapshot) {
|
|
117
|
+
const prefix = `background job ${snapshot.id}`;
|
|
118
|
+
const detail = ` (${snapshot.kind}: ${snapshot.label}) finished ${statusLine(snapshot)}`;
|
|
119
|
+
const action = "\nDone; job_output.";
|
|
120
|
+
const complete = `${prefix}${detail}. Read its output with job_output.`;
|
|
121
|
+
const maxBytes = snapshot.outputLimitBytes;
|
|
122
|
+
if (maxBytes === void 0 || encoder.encode(complete).byteLength <= maxBytes) return complete;
|
|
123
|
+
const omitted = "\n[notice truncated]";
|
|
124
|
+
const fixed = `${prefix}${omitted}${action}`;
|
|
125
|
+
const fixedBytes = encoder.encode(fixed).byteLength;
|
|
126
|
+
if (fixedBytes <= maxBytes) return fixedBytes === maxBytes ? fixed : `${prefix}${retainHead(detail, maxBytes - fixedBytes)}${omitted}${action}`;
|
|
127
|
+
const compact = `${prefix}${action}`;
|
|
128
|
+
if (encoder.encode(compact).byteLength <= maxBytes) return compact;
|
|
129
|
+
const actionBytes = encoder.encode(action).byteLength;
|
|
130
|
+
if (actionBytes >= maxBytes) return retainTail(action, maxBytes);
|
|
131
|
+
return `${retainHead(prefix, maxBytes - actionBytes)}${action}`;
|
|
132
|
+
}
|
|
133
|
+
function rawSingleText(content) {
|
|
134
|
+
if (content.length !== 1) return void 0;
|
|
135
|
+
const block = content[0];
|
|
136
|
+
if (block?.type !== "text") return void 0;
|
|
137
|
+
return block.text;
|
|
138
|
+
}
|
|
139
|
+
function boundSingleText(content, maxBytes) {
|
|
140
|
+
const text = rawSingleText(content);
|
|
141
|
+
if (text === void 0) return void 0;
|
|
142
|
+
return [{
|
|
143
|
+
type: "text",
|
|
144
|
+
text: fitWithSuffix(text, "", maxBytes, "\n[result truncated]")
|
|
145
|
+
}];
|
|
146
|
+
}
|
|
147
|
+
function visibleOutputLimit(ctx, exec) {
|
|
148
|
+
if (exec.name !== "job_output" && exec.name !== "job_kill") return void 0;
|
|
149
|
+
const jobId = exec.arguments?.job_id;
|
|
150
|
+
if (typeof jobId !== "string" || jobId.length === 0) return void 0;
|
|
151
|
+
return ctx.jobs.list(exec.agent).find((snapshot) => snapshot.id === jobId)?.outputLimitBytes;
|
|
152
|
+
}
|
|
153
|
+
/** Validate the non-empty constraint that ParameterSchemaSpec cannot express. */
|
|
154
|
+
function validateJobId(value) {
|
|
155
|
+
if (value.length === 0) throw new Error(`invalid job_id: expected a non-empty string, got ${JSON.stringify(value)}`);
|
|
156
|
+
return JobId(value);
|
|
157
|
+
}
|
|
158
|
+
/** Pending presentation shared by the three generic job controls. */
|
|
159
|
+
function presentTaskCall(title, kind, rawInput) {
|
|
160
|
+
return {
|
|
161
|
+
card: "generic",
|
|
162
|
+
title,
|
|
163
|
+
kind,
|
|
164
|
+
...rawInput !== void 0 ? { rawInput } : {}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function apply(ctx, config) {
|
|
168
|
+
const waitDefault = config.waitTimeoutMs ?? 3e4;
|
|
169
|
+
const waitCap = config.maxWaitTimeoutMs ?? 6e5;
|
|
170
|
+
const delivery = config.completionDelivery ?? "wakeup";
|
|
171
|
+
const wakeBudget = config.maxConsecutiveWakes ?? 3;
|
|
172
|
+
const spentWakes = /* @__PURE__ */ new WeakMap();
|
|
173
|
+
if (waitDefault > waitCap) throw new Error(`tool-jobs: waitTimeoutMs (${waitDefault}) exceeds maxWaitTimeoutMs (${waitCap})`);
|
|
174
|
+
if (!Number.isSafeInteger(wakeBudget)) throw new Error(`tool-jobs: maxConsecutiveWakes (${wakeBudget}) must be a whole number of turns`);
|
|
175
|
+
if (delivery === "wakeup") ctx.on("agent/inbox/claimed", ({ agent, message }) => {
|
|
176
|
+
if (message.source.kind === "user") spentWakes.delete(agent);
|
|
177
|
+
});
|
|
178
|
+
const outputLimits = /* @__PURE__ */ new WeakMap();
|
|
179
|
+
ctx.on("tools/pre-execute", (exec, next) => {
|
|
180
|
+
const maxBytes = visibleOutputLimit(ctx, exec);
|
|
181
|
+
if (maxBytes !== void 0) outputLimits.set(exec, maxBytes);
|
|
182
|
+
return next();
|
|
183
|
+
}, { prepend: true });
|
|
184
|
+
const finalizeTaskContent = (exec, result) => {
|
|
185
|
+
const maxBytes = outputLimits.get(exec) ?? visibleOutputLimit(ctx, exec);
|
|
186
|
+
outputLimits.delete(exec);
|
|
187
|
+
if (maxBytes === void 0) return void 0;
|
|
188
|
+
if (exec.name === "job_output" && !result.isError) {
|
|
189
|
+
const value = result.value;
|
|
190
|
+
const body = value.text.length > 0 ? value.text : "(no new output)";
|
|
191
|
+
const content = body.endsWith("\n") ? body.slice(0, -1) : body;
|
|
192
|
+
const suffix = `\n${statusLine(value.job)}`;
|
|
193
|
+
if (rawSingleText(result.content) === `${content}${suffix}`) return [{
|
|
194
|
+
type: "text",
|
|
195
|
+
text: fitWithSuffix(content, suffix, maxBytes, "\n[output truncated]")
|
|
196
|
+
}];
|
|
197
|
+
}
|
|
198
|
+
return boundSingleText(result.content, maxBytes);
|
|
199
|
+
};
|
|
200
|
+
ctx.jobs.attachController("tool-jobs");
|
|
201
|
+
ctx.systemPrompt.section({
|
|
202
|
+
name: "tool:jobs",
|
|
203
|
+
order: ctx.systemPrompt.getSectionOrder("TOOL_JOBS"),
|
|
204
|
+
text: "Track every background job id you start. You are notified in-session when a job finishes — do not busy-poll or sleep on one; keep working on independent steps and do not duplicate a running job's work. Before giving a final answer, collect every still-relevant job with job_output (set wait: true only when you are genuinely blocked on it), and job_kill jobs that stopped mattering."
|
|
205
|
+
});
|
|
206
|
+
ctx.jobs.onJobDone((snapshot, owner) => {
|
|
207
|
+
if (snapshot.reported || owner === void 0) return;
|
|
208
|
+
const message = createUserMessage({
|
|
209
|
+
content: [{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: fitCompletionNotice(snapshot)
|
|
212
|
+
}],
|
|
213
|
+
source: {
|
|
214
|
+
kind: "plugin",
|
|
215
|
+
plugin: "tool-jobs",
|
|
216
|
+
form: "notice",
|
|
217
|
+
summary: completionSummary(snapshot)
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
const spent = spentWakes.get(owner) ?? 0;
|
|
221
|
+
if (delivery === "wakeup" && owner.status === "idle" && spent < wakeBudget) {
|
|
222
|
+
spentWakes.set(owner, spent + 1);
|
|
223
|
+
owner.followup(message);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
owner.inject(message);
|
|
227
|
+
});
|
|
228
|
+
ctx.tools.register(defineTool({
|
|
229
|
+
name: "job_output",
|
|
230
|
+
description: "Read a background job. Stream jobs return only output since the previous read; final-output jobs return their result after settlement. Every response ends with `[status: ...]`. Reads are non-blocking unless `wait: true`, which waits up to the configured cap.",
|
|
231
|
+
parameters: {
|
|
232
|
+
job_id: {
|
|
233
|
+
type: "string",
|
|
234
|
+
required: true,
|
|
235
|
+
description: "Job id returned by the tool that started the background work."
|
|
236
|
+
},
|
|
237
|
+
wait: {
|
|
238
|
+
type: "boolean",
|
|
239
|
+
description: "Block until the job reaches a terminal status or the timeout expires. A timed-out wait returns [status: running] and leaves the job alive."
|
|
240
|
+
},
|
|
241
|
+
timeout_ms: {
|
|
242
|
+
type: "number",
|
|
243
|
+
description: "Max wait in milliseconds (only meaningful with wait: true). Defaults to the configured wait timeout; capped by the configured maximum."
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
finalizeContent: finalizeTaskContent,
|
|
247
|
+
output: {
|
|
248
|
+
schema: {
|
|
249
|
+
type: "object",
|
|
250
|
+
additionalProperties: false,
|
|
251
|
+
properties: {
|
|
252
|
+
text: {
|
|
253
|
+
type: "string",
|
|
254
|
+
required: true
|
|
255
|
+
},
|
|
256
|
+
job: {
|
|
257
|
+
...PUBLIC_TASK_SCHEMA,
|
|
258
|
+
required: true
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
render: (_args, value) => {
|
|
263
|
+
const body = value.text.length > 0 ? value.text : "(no new output)";
|
|
264
|
+
return [{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: `${body}${body.endsWith("\n") ? "" : "\n"}${statusLine(value.job)}`
|
|
267
|
+
}];
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
async execute(args, exec) {
|
|
271
|
+
const id = validateJobId(args.job_id);
|
|
272
|
+
if (args.wait === true) {
|
|
273
|
+
const timeout = Math.min(args.timeout_ms ?? waitDefault, waitCap);
|
|
274
|
+
await ctx.jobs.wait(id, timeout, exec.agent, exec.signal);
|
|
275
|
+
}
|
|
276
|
+
const read = ctx.jobs.read(id, exec.agent);
|
|
277
|
+
return {
|
|
278
|
+
text: read.text,
|
|
279
|
+
job: publicJob(read.snapshot)
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
presentCall: (args) => presentTaskCall(`Read output from background job ${args.job_id}`, "read", args.job_id)
|
|
283
|
+
}));
|
|
284
|
+
ctx.tools.register(defineTool({
|
|
285
|
+
name: "job_list",
|
|
286
|
+
description: "List your background jobs (running and finished) with their ids, kinds, and statuses.",
|
|
287
|
+
parameters: {},
|
|
288
|
+
output: {
|
|
289
|
+
schema: {
|
|
290
|
+
type: "array",
|
|
291
|
+
items: PUBLIC_TASK_SCHEMA
|
|
292
|
+
},
|
|
293
|
+
render: (_args, jobs) => [{
|
|
294
|
+
type: "text",
|
|
295
|
+
text: jobs.length === 0 ? "(no background jobs)" : jobs.map((t) => `${t.id} [${t.kind}] ${t.status} — ${t.label}`).join("\n")
|
|
296
|
+
}]
|
|
297
|
+
},
|
|
298
|
+
execute(_args, exec) {
|
|
299
|
+
const jobs = ctx.jobs.list(exec.agent);
|
|
300
|
+
return Promise.resolve(jobs.map(publicJob));
|
|
301
|
+
},
|
|
302
|
+
presentCall: () => presentTaskCall("List background jobs", "read")
|
|
303
|
+
}));
|
|
304
|
+
ctx.tools.register(defineTool({
|
|
305
|
+
name: "job_kill",
|
|
306
|
+
description: "Request cancellation of a running background job by job id. Returns immediately; the job settles as killed once its work actually stops.",
|
|
307
|
+
parameters: {
|
|
308
|
+
job_id: {
|
|
309
|
+
type: "string",
|
|
310
|
+
required: true,
|
|
311
|
+
description: "Job id returned by the tool that started the background work."
|
|
312
|
+
},
|
|
313
|
+
reason: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "Optional short reason, recorded in the log and forwarded to the job."
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
finalizeContent: finalizeTaskContent,
|
|
319
|
+
output: {
|
|
320
|
+
schema: {
|
|
321
|
+
type: "object",
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
properties: {
|
|
324
|
+
outcome: {
|
|
325
|
+
type: "string",
|
|
326
|
+
required: true,
|
|
327
|
+
enum: ["cancellation-requested", "already-finished"]
|
|
328
|
+
},
|
|
329
|
+
job: {
|
|
330
|
+
...PUBLIC_TASK_SCHEMA,
|
|
331
|
+
required: true
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
render: (_args, value) => [{
|
|
336
|
+
type: "text",
|
|
337
|
+
text: value.outcome === "already-finished" ? `job ${value.job.id} had already finished ${statusLine(value.job)}` : `requested cancellation of job ${value.job.id}`
|
|
338
|
+
}]
|
|
339
|
+
},
|
|
340
|
+
execute(args, exec) {
|
|
341
|
+
const id = validateJobId(args.job_id);
|
|
342
|
+
const result = ctx.jobs.kill(id, exec.agent, args.reason);
|
|
343
|
+
const snapshot = publicJob(ctx.jobs.get(id, exec.agent));
|
|
344
|
+
return Promise.resolve({
|
|
345
|
+
outcome: result === "already-finished" ? "already-finished" : "cancellation-requested",
|
|
346
|
+
job: snapshot
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
presentCall: (args) => presentTaskCall(`Kill background job ${args.job_id}`, "execute", args.job_id)
|
|
350
|
+
}));
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
export { Config, apply, inject, name, statusLine };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing `job_output`, `job_list`, and `job_kill` tools over
|
|
3
|
+
* `ctx.jobs`. Loading the plugin attaches the controller required by
|
|
4
|
+
* producers. It also delivers unreported completions to the owning agent:
|
|
5
|
+
* injected into a busy owner's next step, or opening a turn on an idle one
|
|
6
|
+
* under the default `wakeup` delivery, bounded per owner.
|
|
7
|
+
* @module @x1a0f3n9/dsh-tool-jobs
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
10
|
+
import z from '@deepseek-ai/schemastery';
|
|
11
|
+
import type { JobSnapshot } from '@x1a0f3n9/dsh-jobs';
|
|
12
|
+
export declare const name = "tool-jobs";
|
|
13
|
+
export declare const inject: string[];
|
|
14
|
+
/**
|
|
15
|
+
* How an unreported completion reaches an owner that is already idle: `wakeup`
|
|
16
|
+
* opens a turn for it, `quiet` leaves it pending until something else wakes the
|
|
17
|
+
* owner. A busy owner is injected either way.
|
|
18
|
+
*/
|
|
19
|
+
export type CompletionDelivery = 'quiet' | 'wakeup';
|
|
20
|
+
/** Configures bounded `job_output` waits and completion-notice delivery. */
|
|
21
|
+
export interface Config {
|
|
22
|
+
/** Wait duration applied when `job_output` sets `wait` without `timeout_ms` (default 30s). */
|
|
23
|
+
waitTimeoutMs?: number;
|
|
24
|
+
/** Hard cap on any single wait; a larger model-supplied `timeout_ms` is clamped down to it (default 10min). */
|
|
25
|
+
maxWaitTimeoutMs?: number;
|
|
26
|
+
/** Whether a completion opens a turn on an idle owner (default `wakeup`). */
|
|
27
|
+
completionDelivery?: CompletionDelivery;
|
|
28
|
+
/**
|
|
29
|
+
* Turns one owner may have opened by completion wakes before the next
|
|
30
|
+
* notice degrades to injection, reset by any user-authored input (default 3).
|
|
31
|
+
* Bounds the self-exciting chain where a woken turn starts the job whose
|
|
32
|
+
* completion wakes it again.
|
|
33
|
+
*/
|
|
34
|
+
maxConsecutiveWakes?: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const Config: z<Config>;
|
|
37
|
+
/** Task state safe for model-authored programs; ownership/bookkeeping fields are omitted. */
|
|
38
|
+
export interface PublicJobSnapshot {
|
|
39
|
+
id: string;
|
|
40
|
+
kind: string;
|
|
41
|
+
label: string;
|
|
42
|
+
status: JobSnapshot['status'];
|
|
43
|
+
detail?: string;
|
|
44
|
+
startedAt: number;
|
|
45
|
+
finishedAt?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render generic status with optional producer detail.
|
|
49
|
+
* @param snapshot - job state to render.
|
|
50
|
+
* @returns a bracketed status line.
|
|
51
|
+
*/
|
|
52
|
+
export declare function statusLine(snapshot: Pick<JobSnapshot, 'status' | 'detail'>): string;
|
|
53
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x1a0f3n9/dsh-tool-jobs",
|
|
3
|
+
"description": "Model-facing background job control tools (job_output, job_list, job_kill) over the ctx.jobs registry",
|
|
4
|
+
"version": "0.1.5-rc.2",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/jobs/tool-jobs"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./src/*": "./src/*",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib/index.js",
|
|
26
|
+
"lib/types/**/*.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@x1a0f3n9/dsh-agent": "^0.1.5-rc.2",
|
|
34
|
+
"@x1a0f3n9/dsh-llm": "^0.1.5-rc.2",
|
|
35
|
+
"@x1a0f3n9/dsh-jobs": "^0.1.5-rc.2",
|
|
36
|
+
"@x1a0f3n9/dsh-system-prompt": "^0.1.5-rc.2",
|
|
37
|
+
"@x1a0f3n9/dsh-tools": "^0.1.5-rc.2",
|
|
38
|
+
"@x1a0f3n9/dsh-output-retention": "^0.1.5-rc.2",
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@x1a0f3n9/dsh-agent": "^0.1.5-rc.2",
|
|
43
|
+
"@x1a0f3n9/dsh-llm": "^0.1.5-rc.2",
|
|
44
|
+
"@x1a0f3n9/dsh-session": "^0.1.5-rc.2",
|
|
45
|
+
"@x1a0f3n9/dsh-jobs-local": "^0.1.5-rc.2",
|
|
46
|
+
"@x1a0f3n9/dsh-output-retention": "^0.1.5-rc.2",
|
|
47
|
+
"@x1a0f3n9/dsh-jobs": "^0.1.5-rc.2",
|
|
48
|
+
"@x1a0f3n9/dsh-tools": "^0.1.5-rc.2",
|
|
49
|
+
"@x1a0f3n9/dsh-system-prompt": "^0.1.5-rc.2",
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
51
|
+
}
|
|
52
|
+
}
|