@qlzt/dsh-spill-policy 0.0.0-stage → 0.2.1-alpha.3
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 +82 -0
- package/README.md +165 -2
- package/README.zh.md +166 -0
- package/lib/index.js +262 -0
- package/lib/types/index.d.ts +27 -0
- package/lib/types/index.js +138 -0
- package/lib/types/notice.d.ts +19 -0
- package/lib/types/notice.js +59 -0
- package/lib/types/retention.d.ts +24 -0
- package/lib/types/retention.js +88 -0
- package/lib/types/types.d.ts +25 -0
- package/lib/types/types.js +13 -0
- package/package.json +73 -4
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,82 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/spill/spill-policy/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 07748986630e8da3
|
|
7
|
+
zh: 4ff004a12b883fdf
|
|
8
|
+
/deepseek-ai-dsh-spill-policy:
|
|
9
|
+
en: e3d75c128bb2f4b9
|
|
10
|
+
zh: 15daa9f8f3efaabd
|
|
11
|
+
/deepseek-ai-dsh-spill-policy/summary:
|
|
12
|
+
en: 225c013f0f230c37
|
|
13
|
+
zh: 30634801496c205f
|
|
14
|
+
/deepseek-ai-dsh-spill-policy/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/deepseek-ai-dsh-spill-policy/use-this-package:
|
|
18
|
+
en: 52e3b36ee5b98e7f
|
|
19
|
+
zh: 98380821aa7cde9d
|
|
20
|
+
/deepseek-ai-dsh-spill-policy/use-this-package/minimal-configuration:
|
|
21
|
+
en: a5d85ae7f7962b9e
|
|
22
|
+
zh: 73c44a9413c3c8cf
|
|
23
|
+
/deepseek-ai-dsh-spill-policy/use-this-package/what-the-model-sees:
|
|
24
|
+
en: 7cb5bba705975712
|
|
25
|
+
zh: 26adbdee1252d2bb
|
|
26
|
+
/deepseek-ai-dsh-spill-policy/use-this-package/which-results-are-affected:
|
|
27
|
+
en: c419117e6b5f84f8
|
|
28
|
+
zh: 1a43afb623346603
|
|
29
|
+
/deepseek-ai-dsh-spill-policy/use-this-package/best-effort-failure-behavior:
|
|
30
|
+
en: b4d94d99a8a9e52f
|
|
31
|
+
zh: d5cec6295cbf5ef6
|
|
32
|
+
/deepseek-ai-dsh-spill-policy/use-this-package/the-durable-log-copy:
|
|
33
|
+
en: b72e6e88ada5a957
|
|
34
|
+
zh: 5c6167651618738a
|
|
35
|
+
/deepseek-ai-dsh-spill-policy/understand-the-implementation:
|
|
36
|
+
en: bb7cedc768113c0e
|
|
37
|
+
zh: ca05828dfe63b233
|
|
38
|
+
/deepseek-ai-dsh-spill-policy/understand-the-implementation/design-philosophy:
|
|
39
|
+
en: a8e0d6d0651401c0
|
|
40
|
+
zh: 0e159fc19caf9300
|
|
41
|
+
/deepseek-ai-dsh-spill-policy/understand-the-implementation/the-two-arms:
|
|
42
|
+
en: a9233181feb1afb4
|
|
43
|
+
zh: e54f1498a0dea843
|
|
44
|
+
/deepseek-ai-dsh-spill-policy/understand-the-implementation/shared-notice-ownership:
|
|
45
|
+
en: 916aa902ea376c42
|
|
46
|
+
zh: 51fbf25c1f1560d7
|
|
47
|
+
/deepseek-ai-dsh-spill-policy/understand-the-implementation/source-map:
|
|
48
|
+
en: 355ca3b01de5233e
|
|
49
|
+
zh: 19a7c28a57171573
|
|
50
|
+
/deepseek-ai-dsh-spill-policy/understand-the-implementation/failure-modes:
|
|
51
|
+
en: 5640d59fdf0e064f
|
|
52
|
+
zh: 7f92c52a1bdf5bff
|
|
53
|
+
/deepseek-ai-dsh-spill-policy/further-exploration:
|
|
54
|
+
en: 2b5860d2cc9a6523
|
|
55
|
+
zh: 006157ad7bc92891
|
|
56
|
+
/deepseek-ai-dsh-spill-policy/model-experience:
|
|
57
|
+
en: 215e7ba838619b7b
|
|
58
|
+
zh: a311da3843709f90
|
|
59
|
+
/deepseek-ai-dsh-spill-policy/model-experience/oversized-text-and-image-results:
|
|
60
|
+
en: 99c96010474af9cf
|
|
61
|
+
zh: 9a25fa709f21a494
|
|
62
|
+
/deepseek-ai-dsh-spill-policy/model-experience/oversized-text-and-image-results/what-the-model-sees:
|
|
63
|
+
en: a701c098db84aff5
|
|
64
|
+
zh: 2b04abc792def1a2
|
|
65
|
+
/deepseek-ai-dsh-spill-policy/model-experience/oversized-text-and-image-results/token-effect:
|
|
66
|
+
en: 02ec4218006ecb65
|
|
67
|
+
zh: 62f1d481ebf0c665
|
|
68
|
+
/deepseek-ai-dsh-spill-policy/model-experience/oversized-text-and-image-results/kv-cache-effect:
|
|
69
|
+
en: f5d5f97a153f7c70
|
|
70
|
+
zh: bee82b41983a66a0
|
|
71
|
+
/deepseek-ai-dsh-spill-policy/known-limitations-and-deferred-work:
|
|
72
|
+
en: bd38d2abae4cb4e6
|
|
73
|
+
zh: 29acb530988ee2bd
|
|
74
|
+
/deepseek-ai-dsh-spill-policy/known-limitations-and-deferred-work/dev-note:
|
|
75
|
+
en: a63e51f03bdb279f
|
|
76
|
+
zh: 191a48d3bfc4a792
|
|
77
|
+
/deepseek-ai-dsh-spill-policy/known-limitations-and-deferred-work/dev-note/future-per-tool-configuration:
|
|
78
|
+
en: 7e6c4300e87f3f07
|
|
79
|
+
zh: 7b97be5218905ecc
|
|
80
|
+
/deepseek-ai-dsh-spill-policy/known-limitations-and-deferred-work/dev-note/future-earlier-spill:
|
|
81
|
+
en: dbb9f9fe93996893
|
|
82
|
+
zh: d9d3c1a46ea67595
|
package/README.md
CHANGED
|
@@ -1,3 +1,166 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Tool-result retention with a shared text/image token budget and readable recovery files."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @deepseek-ai/dsh-spill-policy
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Keep oversized text and image results within a shared estimated token budget. The model receives ordered head/tail content and a path to the complete result. Images remain in attachment storage; the result file records their readable paths. Omitting `maxInlineTokens` disables retention, and recovery failures leave the original content visible.
|
|
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
|
+
Mount the policy alongside a spill backend. Text and images share the configured budget after post-execute policy accepts the result.
|
|
29
|
+
|
|
30
|
+
### Minimal configuration
|
|
31
|
+
|
|
32
|
+
Load a spill backend and set `maxInlineTokens` in estimated tokens:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- name: '@deepseek-ai/dsh-spill-local'
|
|
36
|
+
- name: '@deepseek-ai/dsh-spill-policy'
|
|
37
|
+
config:
|
|
38
|
+
maxInlineTokens: 12500
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Field | Default | Meaning |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `maxInlineTokens` | omitted | Estimated token cap for retained text, images, image descriptions, and notices; omission disables retention |
|
|
44
|
+
|
|
45
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-spill-policy) is the exhaustive source for every accepted field. A negative or fractional cap fails plugin load rather than corrupting per-call behavior.
|
|
46
|
+
|
|
47
|
+
### What the model sees
|
|
48
|
+
|
|
49
|
+
Oversized results keep their original order. Each end receives half the budget remaining after omission notices; text may be split, while each image is retained or omitted whole. Images inside the omitted interval are omitted too. A successful replacement stays within the configured token estimate:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
<retained head/tail preview>
|
|
53
|
+
|
|
54
|
+
(Omitted N bytes. Full formatted result stored at: /…/session-…/…-web_fetch.txt. Use read with offset/limit, or grep this path to search within it.)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The notice also reports omitted image counts. A notice-only result is allowed when no preview fits; if the notice itself exceeds the cap, the original content stays visible. The full result file keeps all accepted text and an attachment path at each image position, so the model can use `read` and then `read_image`. Attachment bytes are not copied into this file. Local attachment objects persist independently of spill cleanup.
|
|
58
|
+
|
|
59
|
+
### Which results are affected
|
|
60
|
+
|
|
61
|
+
The policy accepts text/image sequences. Results within budget, `read`, blocked decisions, value replacements, and other block types pass through. Text-only nested results are bounded only in their log copies. Provider or tool limits applied before this policy cannot be recovered here.
|
|
62
|
+
|
|
63
|
+
### Best-effort failure behavior
|
|
64
|
+
|
|
65
|
+
A missing owner or spill backend, failed storage, missing route image pricing, or unavailable execution-world image path logs a warning and keeps the original content. The policy never substitutes an unreadable path for an image.
|
|
66
|
+
|
|
67
|
+
### The durable log copy
|
|
68
|
+
|
|
69
|
+
PTC programs receive complete canonical values. Image-bearing sub-results are bounded before forwarding to the model; when every image is omitted, the model still receives the retained text and recovery notice. The dispatch log uses the same retained content. Text-only sub-call logs, including `read`, are bounded asynchronously without delaying program values.
|
|
70
|
+
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
<a id="understand-the-implementation"></a>
|
|
74
|
+
## Understand the implementation
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>Implementation internals — click to expand</summary>
|
|
78
|
+
|
|
79
|
+
This section explains the design decisions behind the policy; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
80
|
+
|
|
81
|
+
### Design philosophy
|
|
82
|
+
|
|
83
|
+
The pure retention function selects ordered content by cost; the plugin owns policy, recovery text, and storage calls. Text uses the existing token-meter estimate. Images use the active route's `imageRequestPricing`, including descriptor text. DeepSeek routes reuse the provider's image-dimension calculator. The budget is an estimate, not an exact tokenizer guarantee.
|
|
84
|
+
|
|
85
|
+
### The two arms
|
|
86
|
+
|
|
87
|
+
The prepended `tools/post-execute` listener delegates before bounding accepted content. `tools/ptc-dispatch-log` shares the same helper. MCP's `projectContent` installs real image blocks before these policies; later content replacement, value replacement, or blocking remains authoritative.
|
|
88
|
+
|
|
89
|
+
<a id="shared-notice-ownership"></a>
|
|
90
|
+
### Shared notice ownership
|
|
91
|
+
|
|
92
|
+
The browser-safe `./notice` entry owns `formatSpillNotice(omitted, ref, images)` and `hasSpillNotice(text)`. It recognizes both historical byte-only notices and notices with whole-image counts without changing recorded text.
|
|
93
|
+
|
|
94
|
+
### Source map
|
|
95
|
+
|
|
96
|
+
| File | Role |
|
|
97
|
+
|---|---|
|
|
98
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: `Config` validation, the two waterfall listeners, the shared replacement helper |
|
|
99
|
+
| [`src/notice.ts`](src/notice.ts) | Browser-safe notice formatting and recognition, published as `./notice` |
|
|
100
|
+
| [`src/retention.ts`](src/retention.ts) | Pure ordered text/image head-tail retention |
|
|
101
|
+
|
|
102
|
+
### Failure modes
|
|
103
|
+
|
|
104
|
+
Recovery or pricing failures preserve the input and log the reason. Negative, fractional, or unsafe-integer budgets fail at plugin load. Results containing unsupported block types remain unchanged.
|
|
105
|
+
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
-----
|
|
109
|
+
|
|
110
|
+
<a id="further-exploration"></a>
|
|
111
|
+
## Further Exploration
|
|
112
|
+
|
|
113
|
+
Read these pages when the package-level contract is not enough.
|
|
114
|
+
|
|
115
|
+
- [Spill storage service](../spill/README.md) — the `saveText` contract behind the policy's replacement.
|
|
116
|
+
- [dsh-spill-local](../spill-local/README.md) — the local backend that stores the spilled text.
|
|
117
|
+
- [Token meter](../../llm/token-meter/README.md) — shared text estimates and route image accounting.
|
|
118
|
+
- [Tool output spill decision](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) — the capability boundary and design rationale.
|
|
119
|
+
|
|
120
|
+
-----
|
|
121
|
+
|
|
122
|
+
<a id="model-experience"></a>
|
|
123
|
+
## Model Experience
|
|
124
|
+
|
|
125
|
+
### Oversized text and image results
|
|
126
|
+
|
|
127
|
+
#### What the model sees
|
|
128
|
+
|
|
129
|
+
The retained prefix and suffix keep image order, with `[...]` at the omitted interval. The final notice names omitted text bytes, optional whole-image counts, and the complete-result path. Reading that file reveals the omitted text and image addresses.
|
|
130
|
+
|
|
131
|
+
#### Token effect
|
|
132
|
+
|
|
133
|
+
A successful replacement fits `maxInlineTokens` under the shared text estimate and active route's image calculator, including notices and image descriptor text. Provider-reported usage remains authoritative.
|
|
134
|
+
|
|
135
|
+
#### KV Cache effect
|
|
136
|
+
|
|
137
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
138
|
+
|
|
139
|
+
## Known Limitations and Deferred Work
|
|
140
|
+
|
|
141
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
These limits define when the policy cannot help. They are current package constraints.
|
|
145
|
+
|
|
146
|
+
- **Text recognition cannot authenticate output** — a tool can print the same notice text; `hasSpillNotice` identifies a text convention, not proof that the policy saved a result.
|
|
147
|
+
- **Unavailable recovery or pricing** — images require a route calculator and execution-readable attachment paths; otherwise the original content stays visible. Unsupported blocks, blocked feedback, and `read` also pass through.
|
|
148
|
+
- **A notice that cannot fit disables replacement for that call** — a tiny cap or long locator leaves the oversized original inline after the backend has already saved an unreferenced spill.
|
|
149
|
+
|
|
150
|
+
<a id="dev-note"></a>
|
|
151
|
+
### Dev Note
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
155
|
+
|
|
156
|
+
This Dev Note is working context for maintainers: open directions. It is explicitly non-authoritative.
|
|
157
|
+
|
|
158
|
+
#### Future: per-tool configuration
|
|
159
|
+
|
|
160
|
+
Per-tool opt-out or per-tool policy declarations remain deferred; the built-in `read` skip covers the known loop, and a second real tool need would justify configuration.
|
|
161
|
+
|
|
162
|
+
#### Future: earlier spill
|
|
163
|
+
|
|
164
|
+
The policy only sees final accepted content. Earlier provider truncation and tool-owned output limits remain outside its scope.
|
|
165
|
+
|
|
166
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "工具结果保留:文字和图片共享 token 预算,并通过完整结果文件恢复省略内容。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-spill-policy
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
将过大的文字和图片结果限制在共享的估算 token 预算内。模型收到按原顺序保留的首尾内容,以及完整结果文件的路径。图片保存在附件存储中,结果文件记录其可读取路径。省略 `maxInlineTokens` 会禁用策略,无法保存可恢复内容时保留原结果。
|
|
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
|
+
将策略与 spill 后端一起挂载。执行后策略接受结果之后,文字和图片共享配置的预算。
|
|
29
|
+
|
|
30
|
+
### 最小配置
|
|
31
|
+
|
|
32
|
+
挂载 spill 后端,并以估算 token 数设置 `maxInlineTokens`:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- name: '@deepseek-ai/dsh-spill-local'
|
|
36
|
+
- name: '@deepseek-ai/dsh-spill-policy'
|
|
37
|
+
config:
|
|
38
|
+
maxInlineTokens: 12500
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| 字段 | 默认值 | 含义 |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `maxInlineTokens` | 省略 | 保留的文字、图片、图片说明和提示的估算 token 上限;省略时禁用策略 |
|
|
44
|
+
|
|
45
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-spill-policy)是每个受支持字段的穷尽式真源。负数或小数上限会让插件加载失败,而不是破坏每次调用的行为。
|
|
46
|
+
|
|
47
|
+
### 模型看到什么
|
|
48
|
+
|
|
49
|
+
过大的结果保持原始顺序。扣除省略提示后,两端各使用剩余预算的一半;文字可以切分,图片整张保留或省略。省略区间中的图片也会被省略。成功替换的内容不超过配置的 token 估算预算:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
<retained head/tail preview>
|
|
53
|
+
|
|
54
|
+
(Omitted N bytes. Full formatted result stored at: /…/session-…/…-web_fetch.txt. Use read with offset/limit, or grep this path to search within it.)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
提示也会报告省略的图片数量。预算容不下预览时允许只返回提示;连提示也超出上限时保留原内容。完整结果文件保存全部已接受文字,并在每张图片的位置记录附件路径,模型可先用 `read` 读取,再用 `read_image` 查看。图片字节不复制到这个文件中。本地附件对象的持久保存独立于 spill 文件清理。
|
|
58
|
+
|
|
59
|
+
### 哪些结果会受影响
|
|
60
|
+
|
|
61
|
+
策略接受文字和图片序列。预算内结果、`read`、被阻止的决策、值替换以及其他内容块类型会原样通过。纯文本嵌套结果只限制日志副本。提供方或工具在此前应用的限制无法在这里恢复。
|
|
62
|
+
|
|
63
|
+
### 尽力而为的故障行为
|
|
64
|
+
|
|
65
|
+
缺少归属或 spill 后端、存储失败、缺少模型图片计量或图片路径无法在执行环境读取时,策略记录警告并保留原内容。策略不会用无法读取的路径替换图片。
|
|
66
|
+
|
|
67
|
+
### 持久日志副本
|
|
68
|
+
|
|
69
|
+
PTC 程序收到完整的规范值。含图片的子结果在转发给模型前设定上限;全部图片被省略时,模型仍会收到保留的文字和读取提示。分发日志使用相同的保留内容。纯文本子调用的日志,包括 `read`,异步设定上限,不延迟程序获取返回值。
|
|
70
|
+
|
|
71
|
+
-----
|
|
72
|
+
|
|
73
|
+
<a id="understand-the-implementation"></a>
|
|
74
|
+
## 理解实现
|
|
75
|
+
|
|
76
|
+
<details>
|
|
77
|
+
<summary>实现细节——点击展开</summary>
|
|
78
|
+
|
|
79
|
+
本节解释该策略背后的设计决策;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
80
|
+
|
|
81
|
+
### 设计理念
|
|
82
|
+
|
|
83
|
+
纯保留函数按成本选择有序内容,插件负责策略、读取提示和存储调用。文字使用现有 token-meter 估算。图片使用当前模型的 `imageRequestPricing`,并计入说明文字。DeepSeek 模型复用提供方的图片尺寸计算器。预算是估算值,不保证与实际分词结果完全一致。
|
|
84
|
+
|
|
85
|
+
### 两条分支
|
|
86
|
+
|
|
87
|
+
以 prepend 注册的 `tools/post-execute` 监听器先委托,再限制已接受的内容。`tools/ptc-dispatch-log` 共用同一辅助函数。MCP 的 `projectContent` 在这些策略之前安装真实图片块;后续内容替换、值替换或阻止仍然生效。
|
|
88
|
+
|
|
89
|
+
<a id="shared-notice-ownership"></a>
|
|
90
|
+
### 共享通知的所有权
|
|
91
|
+
|
|
92
|
+
浏览器安全入口 `./notice` 负责 `formatSpillNotice(omitted, ref, images)` 和 `hasSpillNotice(text)`。它识别历史的仅字节提示和包含整张图片数量的提示,不改写已有记录。
|
|
93
|
+
|
|
94
|
+
### 源码地图
|
|
95
|
+
|
|
96
|
+
| 文件 | 职责 |
|
|
97
|
+
|---|---|
|
|
98
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:`Config` 校验、两个 waterfall 监听器、共享替换辅助函数 |
|
|
99
|
+
| [`src/notice.ts`](src/notice.ts) | 浏览器安全的通知格式化与识别,以 `./notice` 发布 |
|
|
100
|
+
| [`src/retention.ts`](src/retention.ts) | 有序图文首尾保留的纯函数 |
|
|
101
|
+
|
|
102
|
+
### 故障模式
|
|
103
|
+
|
|
104
|
+
无法恢复内容或计量失败时保留输入,并记录原因。负数、小数或非安全整数预算会使插件加载失败。包含不支持的内容块类型的结果保持原样。
|
|
105
|
+
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
-----
|
|
109
|
+
|
|
110
|
+
<a id="further-exploration"></a>
|
|
111
|
+
## 进一步探索
|
|
112
|
+
|
|
113
|
+
当包级约定不够用时阅读以下页面。
|
|
114
|
+
|
|
115
|
+
- [spill 存储服务](../spill/README.zh.md)——策略替换背后的 `saveText` 约定。
|
|
116
|
+
- [dsh-spill-local](../spill-local/README.zh.md)——保存 spill 文本的本地后端。
|
|
117
|
+
- [Token meter](../../llm/token-meter/README.zh.md) — 共享文字估算和模型图片计量。
|
|
118
|
+
- [工具输出 spill 决策](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md)——能力边界与设计依据。
|
|
119
|
+
|
|
120
|
+
-----
|
|
121
|
+
|
|
122
|
+
<a id="model-experience"></a>
|
|
123
|
+
## 模型体验
|
|
124
|
+
|
|
125
|
+
### 过大的文字和图片结果
|
|
126
|
+
|
|
127
|
+
#### 模型看到什么
|
|
128
|
+
|
|
129
|
+
保留的前缀和后缀维持图片顺序,并在省略区间显示 `[...]`。末尾提示说明省略的文字字节数、整张图片数量及完整结果路径。读取该文件可以找到省略的文字和图片地址。
|
|
130
|
+
|
|
131
|
+
#### Token 影响
|
|
132
|
+
|
|
133
|
+
成功替换的结果在共享文字估算和当前模型图片计算器下不超过 `maxInlineTokens`,包括提示和图片说明文字。实际用量以提供方报告为准。
|
|
134
|
+
|
|
135
|
+
#### KV Cache 影响
|
|
136
|
+
|
|
137
|
+
仅追加;新可见内容位于可重用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
138
|
+
|
|
139
|
+
## 已知限制与延期工作
|
|
140
|
+
|
|
141
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
这些限制说明策略在哪些情况下无法提供帮助。它们是当前的包约束。
|
|
145
|
+
|
|
146
|
+
- **文本识别无法认证输出来源**——工具也能打印相同的通知文本;`hasSpillNotice` 识别的是文本约定,不能证明策略保存过结果。
|
|
147
|
+
- **无法恢复或计量**:图片要求模型计算器和执行环境可读取的附件路径,否则保留原内容。不支持的内容块、被阻止的反馈和 `read` 也会原样通过。
|
|
148
|
+
- **通知无法容纳时会禁用该次调用的替换**——上限极小或定位信息很长时,后端已经保存了无引用的 spill,但过大的原始结果仍留在内联位置。
|
|
149
|
+
|
|
150
|
+
<a id="dev-note"></a>
|
|
151
|
+
### 开发备注
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
155
|
+
|
|
156
|
+
本开发备注是维护者的工作上下文:开放方向。它明确不具权威性。
|
|
157
|
+
|
|
158
|
+
#### 未来:逐工具配置
|
|
159
|
+
|
|
160
|
+
逐工具选择退出或逐工具策略声明仍然延期;内置的 `read` 跳过已覆盖已知循环,第二个真实工具需求才能证明配置的合理性。
|
|
161
|
+
|
|
162
|
+
#### 未来:更早的 spill
|
|
163
|
+
|
|
164
|
+
策略只处理最终已接受的内容。此前的提供方截断和工具自身的输出限制仍由各自负责。
|
|
165
|
+
|
|
166
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { createUserMessage, resolveImageAttachmentAccess } from "@qlzt/dsh-llm";
|
|
3
|
+
import { estimateContent } from "@qlzt/dsh-token-meter/estimate";
|
|
4
|
+
import { describeOmitted } from "@qlzt/dsh-output-retention";
|
|
5
|
+
//#region lib/types/notice.js
|
|
6
|
+
/** Browser-safe formatting and recognition of persisted spill-policy notices. */
|
|
7
|
+
const OPEN = "(";
|
|
8
|
+
const CLOSE = ")";
|
|
9
|
+
const LOCATION = " Full formatted result stored at: ";
|
|
10
|
+
const GUIDANCE_SEPARATOR = ". ";
|
|
11
|
+
const EXACT_OMISSION = describeOmitted({
|
|
12
|
+
kind: "exact",
|
|
13
|
+
count: 0
|
|
14
|
+
}, "bytes");
|
|
15
|
+
const COUNT_OFFSET = EXACT_OMISSION.indexOf("0");
|
|
16
|
+
EXACT_OMISSION.slice(COUNT_OFFSET + 1);
|
|
17
|
+
/**
|
|
18
|
+
* Format the notice appended to a retained preview, preserving its persisted spelling.
|
|
19
|
+
* @param omitted - bytes omitted by the retention policy.
|
|
20
|
+
* @param ref - saved text locator and retrieval guidance.
|
|
21
|
+
* @param images - number of whole images omitted alongside text.
|
|
22
|
+
* @returns the complete notice without a leading preview separator.
|
|
23
|
+
*/
|
|
24
|
+
function formatSpillNotice(omitted, ref, images = 0) {
|
|
25
|
+
const imageNotice = images > 0 ? ` Omitted ${images} images.` : "";
|
|
26
|
+
return `${OPEN}${describeOmitted(omitted, "bytes")}${imageNotice}${LOCATION}${ref.locator}${GUIDANCE_SEPARATOR}${ref.retrievalHint}${CLOSE}`;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region lib/types/retention.js
|
|
30
|
+
/** Preserve a UTF-16 code point when a text budget cuts between its surrogates. */
|
|
31
|
+
function textSlice(text, length, tail) {
|
|
32
|
+
let cut = tail ? text.length - length : length;
|
|
33
|
+
const previous = text.charCodeAt(cut - 1);
|
|
34
|
+
const current = text.charCodeAt(cut);
|
|
35
|
+
if (previous >= 55296 && previous <= 56319 && current >= 56320 && current <= 57343) cut += tail ? 1 : -1;
|
|
36
|
+
return tail ? text.slice(cut) : text.slice(0, cut);
|
|
37
|
+
}
|
|
38
|
+
/** Largest contiguous text end fitting a monotonic token estimate. */
|
|
39
|
+
function fitText(text, budget, tail, price) {
|
|
40
|
+
let low = 0;
|
|
41
|
+
let high = text.length;
|
|
42
|
+
while (low < high) {
|
|
43
|
+
const middle = Math.ceil((low + high) / 2);
|
|
44
|
+
const candidate = textSlice(text, middle, tail);
|
|
45
|
+
if (candidate.length === 0 || price({
|
|
46
|
+
type: "text",
|
|
47
|
+
text: candidate
|
|
48
|
+
}) <= budget) low = middle;
|
|
49
|
+
else high = middle - 1;
|
|
50
|
+
}
|
|
51
|
+
return textSlice(text, low, tail);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Retain contiguous ends without moving or partially retaining an image.
|
|
55
|
+
* Each end receives half the content budget; unused space at an indivisible
|
|
56
|
+
* image stays unused. Text pricing must be monotonic in retained length.
|
|
57
|
+
* @param content - ordered text and image blocks whose total price exceeds budget.
|
|
58
|
+
* @param budget - non-negative integer token budget after reserving notices.
|
|
59
|
+
* @param price - model-route cost of one retained block, including its framing.
|
|
60
|
+
* @returns retained ends and omitted UTF-8 text bytes and whole images.
|
|
61
|
+
*/
|
|
62
|
+
function retainContent(content, budget, price) {
|
|
63
|
+
const head = [];
|
|
64
|
+
const tail = [];
|
|
65
|
+
let first = 0;
|
|
66
|
+
let last = content.length - 1;
|
|
67
|
+
let headCharacters = 0;
|
|
68
|
+
let remaining = Math.ceil(budget / 2);
|
|
69
|
+
while (first <= last) {
|
|
70
|
+
const block = content[first];
|
|
71
|
+
const cost = price(block);
|
|
72
|
+
if (cost <= remaining) {
|
|
73
|
+
head.push(block);
|
|
74
|
+
remaining -= cost;
|
|
75
|
+
first++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (block.type === "text") {
|
|
79
|
+
const text = fitText(block.text, remaining, false, price);
|
|
80
|
+
headCharacters = text.length;
|
|
81
|
+
if (text.length > 0) head.push({
|
|
82
|
+
type: "text",
|
|
83
|
+
text
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
remaining = Math.floor(budget / 2);
|
|
89
|
+
while (last >= first) {
|
|
90
|
+
const original = content[last];
|
|
91
|
+
const block = last === first && original.type === "text" ? {
|
|
92
|
+
type: "text",
|
|
93
|
+
text: original.text.slice(headCharacters)
|
|
94
|
+
} : original;
|
|
95
|
+
const cost = price(block);
|
|
96
|
+
if (cost <= remaining) {
|
|
97
|
+
tail.push(block);
|
|
98
|
+
remaining -= cost;
|
|
99
|
+
last--;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (block.type === "text") {
|
|
103
|
+
const text = fitText(block.text, remaining, true, price);
|
|
104
|
+
if (text.length > 0) tail.push({
|
|
105
|
+
type: "text",
|
|
106
|
+
text
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
tail.reverse();
|
|
112
|
+
const bytes = (blocks) => blocks.reduce((total, block) => total + (block.type === "text" ? Buffer.byteLength(block.text, "utf8") : 0), 0);
|
|
113
|
+
const images = (blocks) => blocks.filter((block) => block.type === "image").length;
|
|
114
|
+
return {
|
|
115
|
+
head,
|
|
116
|
+
tail,
|
|
117
|
+
omittedBytes: bytes(content) - bytes(head) - bytes(tail),
|
|
118
|
+
omittedImages: images(content) - images(head) - images(tail)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region lib/types/index.js
|
|
123
|
+
/** Cordis plugin identity. */
|
|
124
|
+
const name = "spill-policy";
|
|
125
|
+
/** Tools own both model-facing results and PTC dispatch logs. */
|
|
126
|
+
const inject = ["tools"];
|
|
127
|
+
const Config = z.object({ maxInlineTokens: z.number() });
|
|
128
|
+
/** Narrow the model's ordered content without rewriting unsupported blocks. */
|
|
129
|
+
function retainable(content) {
|
|
130
|
+
return content.every((block) => block.type === "text" || block.type === "image");
|
|
131
|
+
}
|
|
132
|
+
const GAP = {
|
|
133
|
+
type: "text",
|
|
134
|
+
text: "\n\n[...]\n\n"
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Mount token retention for accepted tool results and PTC log copies.
|
|
138
|
+
* @param ctx - tool registry and optional pricing, filesystem, attachment, and spill services.
|
|
139
|
+
* @param config - maximum estimated result tokens; omission disables the plugin.
|
|
140
|
+
*/
|
|
141
|
+
function apply(ctx, config) {
|
|
142
|
+
const cap = config.maxInlineTokens;
|
|
143
|
+
if (cap === void 0) return;
|
|
144
|
+
if (!Number.isSafeInteger(cap) || cap < 0) throw new Error(`spill-policy: maxInlineTokens must be a non-negative integer (got ${cap})`);
|
|
145
|
+
const maxTokens = cap;
|
|
146
|
+
/** Price actual request images and their accompanying descriptor text. */
|
|
147
|
+
function pricing(exec, images) {
|
|
148
|
+
const costs = /* @__PURE__ */ new Map();
|
|
149
|
+
if (images.length > 0) {
|
|
150
|
+
const routed = exec.agent?.session.requestHeader()?.config;
|
|
151
|
+
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
152
|
+
const model = routed?.model ?? exec.agent?.options.model;
|
|
153
|
+
const calculator = provider === void 0 || model === void 0 ? void 0 : ctx.get("llm")?.imageRequestPricing(provider, model);
|
|
154
|
+
if (calculator === void 0) throw new Error("the current model has no image token calculator");
|
|
155
|
+
const prices = calculator.priceImages(images);
|
|
156
|
+
if (prices.length !== images.length) throw new Error("image token calculator returned an inconsistent occurrence count");
|
|
157
|
+
for (const [index, image] of images.entries()) {
|
|
158
|
+
const cost = prices[index];
|
|
159
|
+
costs.set(image, cost.visualTokens + estimateContent([{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: cost.text
|
|
162
|
+
}]));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return (block) => block.type === "image" ? costs.get(block) : estimateContent([block]);
|
|
166
|
+
}
|
|
167
|
+
/** Full ordered text with an execution-readable attachment path at each image position. */
|
|
168
|
+
function fullText(content) {
|
|
169
|
+
return content.map((block) => {
|
|
170
|
+
if (block.type === "text") return block.text;
|
|
171
|
+
const attachments = ctx.get("attachments");
|
|
172
|
+
const fs = ctx.get("fs");
|
|
173
|
+
const access = attachments === void 0 || fs === void 0 ? void 0 : resolveImageAttachmentAccess(attachments, (path) => fs.processPathFromHostPath(path), block.attachment);
|
|
174
|
+
if (access === void 0) throw new Error(`image ${block.attachment.attachmentId} has no readable attachment path`);
|
|
175
|
+
return `\n[Image: ${JSON.stringify(access.readonlyPath)}; ${block.attachment.mediaType}; ${block.attachment.width}x${block.attachment.height}. Use read_image to view it.]\n`;
|
|
176
|
+
}).join("");
|
|
177
|
+
}
|
|
178
|
+
/** Recoverably bound one display copy; failures leave successful tool content visible. */
|
|
179
|
+
async function bound(exec, content, toolName, callId, label) {
|
|
180
|
+
if (!retainable(content)) return void 0;
|
|
181
|
+
try {
|
|
182
|
+
const images = content.filter((block) => block.type === "image");
|
|
183
|
+
const price = pricing(exec, images);
|
|
184
|
+
if (content.reduce((total, block) => total + price(block), 0) <= maxTokens) return void 0;
|
|
185
|
+
const owner = exec.agent?.session.header.id;
|
|
186
|
+
if (owner === void 0) throw new Error(`no session owner for ${toolName} ${label}`);
|
|
187
|
+
const spillStore = ctx.get("spillStore");
|
|
188
|
+
if (spillStore === void 0) throw new Error("no ctx.spillStore backend loaded");
|
|
189
|
+
const ref = await spillStore.saveText({
|
|
190
|
+
owner: { sessionId: owner },
|
|
191
|
+
source: {
|
|
192
|
+
kind: "tool",
|
|
193
|
+
toolName,
|
|
194
|
+
callId,
|
|
195
|
+
label
|
|
196
|
+
},
|
|
197
|
+
suggestedName: `${toolName}.txt`,
|
|
198
|
+
content: fullText(content)
|
|
199
|
+
});
|
|
200
|
+
const totalBytes = content.reduce((total, block) => total + (block.type === "text" ? Buffer.byteLength(block.text, "utf8") : 0), 0);
|
|
201
|
+
const notice = (bytes, count) => ({
|
|
202
|
+
type: "text",
|
|
203
|
+
text: formatSpillNotice({
|
|
204
|
+
kind: "exact",
|
|
205
|
+
count: bytes
|
|
206
|
+
}, ref, count)
|
|
207
|
+
});
|
|
208
|
+
const worstNotice = notice(totalBytes, images.length);
|
|
209
|
+
const reserved = price(GAP) + price({
|
|
210
|
+
type: "text",
|
|
211
|
+
text: `\n\n${worstNotice.text}`
|
|
212
|
+
});
|
|
213
|
+
if (price(worstNotice) > maxTokens) throw new Error(`spill notice for ${toolName} exceeds maxInlineTokens`);
|
|
214
|
+
const retained = retainContent(content, Math.max(0, maxTokens - reserved), price);
|
|
215
|
+
const footer = notice(retained.omittedBytes, retained.omittedImages);
|
|
216
|
+
const result = retained.head.length + retained.tail.length === 0 ? [footer] : [
|
|
217
|
+
...retained.head,
|
|
218
|
+
GAP,
|
|
219
|
+
...retained.tail,
|
|
220
|
+
{
|
|
221
|
+
type: "text",
|
|
222
|
+
text: `\n\n${footer.text}`
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
const merged = [];
|
|
226
|
+
for (const block of result) {
|
|
227
|
+
const previous = merged.at(-1);
|
|
228
|
+
if (block.type === "text" && previous?.type === "text") previous.text += block.text;
|
|
229
|
+
else merged.push(block.type === "text" ? { ...block } : block);
|
|
230
|
+
}
|
|
231
|
+
return merged;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
ctx.logger.warn(`spill-policy: ${String(error)}; keeping the inline content`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
238
|
+
const decision = await next();
|
|
239
|
+
if (decision.kind !== "accept" || Object.hasOwn(decision, "value") || exec.name === "read") return decision;
|
|
240
|
+
const content = decision.content ?? result.content;
|
|
241
|
+
const hasImages = content.some((block) => block.type === "image");
|
|
242
|
+
if (exec.parent !== void 0 && !hasImages) return decision;
|
|
243
|
+
const retained = await bound(exec, content, exec.name, exec.callId, exec.parent === void 0 ? "result" : "dispatch");
|
|
244
|
+
if (retained === void 0) return decision;
|
|
245
|
+
const additionalContexts = [...decision.additionalContexts ?? []];
|
|
246
|
+
if (exec.parent !== void 0 && !result.isError && hasImages && !retained.some((block) => block.type === "image")) additionalContexts.push(createUserMessage({
|
|
247
|
+
content: retained,
|
|
248
|
+
source: { kind: "ptc-mode" }
|
|
249
|
+
}));
|
|
250
|
+
return {
|
|
251
|
+
kind: "accept",
|
|
252
|
+
content: retained,
|
|
253
|
+
...additionalContexts.length > 0 ? { additionalContexts } : {}
|
|
254
|
+
};
|
|
255
|
+
}, { prepend: true });
|
|
256
|
+
ctx.on("tools/ptc-dispatch-log", async (dispatch, next) => {
|
|
257
|
+
const content = await next();
|
|
258
|
+
return await bound(dispatch.exec, content, dispatch.name, dispatch.subCallId, "dispatch") ?? content;
|
|
259
|
+
}, { prepend: true });
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
export { Config, apply, inject, name };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-budgeted tool content with recoverable text and image addresses.
|
|
3
|
+
* Post-execute policies settle before retention; canonical program values
|
|
4
|
+
* remain intact. Missing recovery storage or image pricing keeps the original
|
|
5
|
+
* content and reports the reason through the logger.
|
|
6
|
+
* @module @qlzt/dsh-spill-policy
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
export type { SpillPolicyExec } from './types.ts';
|
|
11
|
+
/** Optional result-retention budget. */
|
|
12
|
+
export interface Config {
|
|
13
|
+
/** Maximum estimated tokens in a retained result, including image descriptors and omission notices. Omitted disables retention. */
|
|
14
|
+
maxInlineTokens?: number;
|
|
15
|
+
}
|
|
16
|
+
/** Cordis plugin identity. */
|
|
17
|
+
export declare const name = "spill-policy";
|
|
18
|
+
/** Tools own both model-facing results and PTC dispatch logs. */
|
|
19
|
+
export declare const inject: string[];
|
|
20
|
+
export declare const Config: z<Config>;
|
|
21
|
+
/**
|
|
22
|
+
* Mount token retention for accepted tool results and PTC log copies.
|
|
23
|
+
* @param ctx - tool registry and optional pricing, filesystem, attachment, and spill services.
|
|
24
|
+
* @param config - maximum estimated result tokens; omission disables the plugin.
|
|
25
|
+
*/
|
|
26
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
27
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import { createUserMessage, resolveImageAttachmentAccess } from '@qlzt/dsh-llm';
|
|
3
|
+
import { estimateContent } from '@qlzt/dsh-token-meter/estimate';
|
|
4
|
+
import { formatSpillNotice } from "./notice.js";
|
|
5
|
+
import { retainContent } from "./retention.js";
|
|
6
|
+
/** Cordis plugin identity. */
|
|
7
|
+
export const name = 'spill-policy';
|
|
8
|
+
/** Tools own both model-facing results and PTC dispatch logs. */
|
|
9
|
+
export const inject = ['tools'];
|
|
10
|
+
export const Config = z.object({ maxInlineTokens: z.number() });
|
|
11
|
+
/** Narrow the model's ordered content without rewriting unsupported blocks. */
|
|
12
|
+
function retainable(content) {
|
|
13
|
+
return content.every(block => block.type === 'text' || block.type === 'image');
|
|
14
|
+
}
|
|
15
|
+
const GAP = { type: 'text', text: '\n\n[...]\n\n' };
|
|
16
|
+
/**
|
|
17
|
+
* Mount token retention for accepted tool results and PTC log copies.
|
|
18
|
+
* @param ctx - tool registry and optional pricing, filesystem, attachment, and spill services.
|
|
19
|
+
* @param config - maximum estimated result tokens; omission disables the plugin.
|
|
20
|
+
*/
|
|
21
|
+
export function apply(ctx, config) {
|
|
22
|
+
const cap = config.maxInlineTokens;
|
|
23
|
+
if (cap === undefined)
|
|
24
|
+
return;
|
|
25
|
+
if (!Number.isSafeInteger(cap) || cap < 0) {
|
|
26
|
+
throw new Error(`spill-policy: maxInlineTokens must be a non-negative integer (got ${cap})`);
|
|
27
|
+
}
|
|
28
|
+
const maxTokens = cap;
|
|
29
|
+
/** Price actual request images and their accompanying descriptor text. */
|
|
30
|
+
function pricing(exec, images) {
|
|
31
|
+
const costs = new Map();
|
|
32
|
+
if (images.length > 0) {
|
|
33
|
+
const routed = exec.agent?.session.requestHeader()?.config;
|
|
34
|
+
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
35
|
+
const model = routed?.model ?? exec.agent?.options.model;
|
|
36
|
+
const calculator = provider === undefined || model === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: ctx.get('llm')?.imageRequestPricing(provider, model);
|
|
39
|
+
if (calculator === undefined)
|
|
40
|
+
throw new Error('the current model has no image token calculator');
|
|
41
|
+
const prices = calculator.priceImages(images);
|
|
42
|
+
if (prices.length !== images.length)
|
|
43
|
+
throw new Error('image token calculator returned an inconsistent occurrence count');
|
|
44
|
+
for (const [index, image] of images.entries()) {
|
|
45
|
+
const cost = prices[index];
|
|
46
|
+
costs.set(image, cost.visualTokens + estimateContent([{ type: 'text', text: cost.text }]));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return block => block.type === 'image' ? costs.get(block) : estimateContent([block]);
|
|
50
|
+
}
|
|
51
|
+
/** Full ordered text with an execution-readable attachment path at each image position. */
|
|
52
|
+
function fullText(content) {
|
|
53
|
+
return content.map((block) => {
|
|
54
|
+
if (block.type === 'text')
|
|
55
|
+
return block.text;
|
|
56
|
+
const attachments = ctx.get('attachments');
|
|
57
|
+
const fs = ctx.get('fs');
|
|
58
|
+
const access = attachments === undefined || fs === undefined ? undefined : resolveImageAttachmentAccess(attachments, path => fs.processPathFromHostPath(path), block.attachment);
|
|
59
|
+
if (access === undefined)
|
|
60
|
+
throw new Error(`image ${block.attachment.attachmentId} has no readable attachment path`);
|
|
61
|
+
return `\n[Image: ${JSON.stringify(access.readonlyPath)}; ${block.attachment.mediaType}; ${block.attachment.width}x${block.attachment.height}. Use read_image to view it.]\n`;
|
|
62
|
+
}).join('');
|
|
63
|
+
}
|
|
64
|
+
/** Recoverably bound one display copy; failures leave successful tool content visible. */
|
|
65
|
+
async function bound(exec, content, toolName, callId, label) {
|
|
66
|
+
if (!retainable(content))
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const images = content.filter((block) => block.type === 'image');
|
|
70
|
+
const price = pricing(exec, images);
|
|
71
|
+
if (content.reduce((total, block) => total + price(block), 0) <= maxTokens)
|
|
72
|
+
return undefined;
|
|
73
|
+
const owner = exec.agent?.session.header.id;
|
|
74
|
+
if (owner === undefined)
|
|
75
|
+
throw new Error(`no session owner for ${toolName} ${label}`);
|
|
76
|
+
const spillStore = ctx.get('spillStore');
|
|
77
|
+
if (spillStore === undefined)
|
|
78
|
+
throw new Error('no ctx.spillStore backend loaded');
|
|
79
|
+
const ref = await spillStore.saveText({
|
|
80
|
+
owner: { sessionId: owner }, source: { kind: 'tool', toolName, callId, label },
|
|
81
|
+
suggestedName: `${toolName}.txt`, content: fullText(content),
|
|
82
|
+
});
|
|
83
|
+
const totalBytes = content.reduce((total, block) => total + (block.type === 'text' ? Buffer.byteLength(block.text, 'utf8') : 0), 0);
|
|
84
|
+
const notice = (bytes, count) => ({
|
|
85
|
+
type: 'text', text: formatSpillNotice({ kind: 'exact', count: bytes }, ref, count),
|
|
86
|
+
});
|
|
87
|
+
const worstNotice = notice(totalBytes, images.length);
|
|
88
|
+
const reserved = price(GAP) + price({ type: 'text', text: `\n\n${worstNotice.text}` });
|
|
89
|
+
if (price(worstNotice) > maxTokens)
|
|
90
|
+
throw new Error(`spill notice for ${toolName} exceeds maxInlineTokens`);
|
|
91
|
+
const retained = retainContent(content, Math.max(0, maxTokens - reserved), price);
|
|
92
|
+
const footer = notice(retained.omittedBytes, retained.omittedImages);
|
|
93
|
+
const result = retained.head.length + retained.tail.length === 0
|
|
94
|
+
? [footer]
|
|
95
|
+
: [...retained.head, GAP, ...retained.tail, { type: 'text', text: `\n\n${footer.text}` }];
|
|
96
|
+
// Adjacent text has one framing cost on the wire and in the spill preview.
|
|
97
|
+
const merged = [];
|
|
98
|
+
for (const block of result) {
|
|
99
|
+
const previous = merged.at(-1);
|
|
100
|
+
if (block.type === 'text' && previous?.type === 'text')
|
|
101
|
+
previous.text += block.text;
|
|
102
|
+
else
|
|
103
|
+
merged.push(block.type === 'text' ? { ...block } : block);
|
|
104
|
+
}
|
|
105
|
+
return merged;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
ctx.logger.warn(`spill-policy: ${String(error)}; keeping the inline content`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
113
|
+
const decision = await next();
|
|
114
|
+
if (decision.kind !== 'accept' || Object.hasOwn(decision, 'value') || exec.name === 'read')
|
|
115
|
+
return decision;
|
|
116
|
+
const content = decision.content ?? result.content;
|
|
117
|
+
const hasImages = content.some(block => block.type === 'image');
|
|
118
|
+
// Text-only PTC bindings retain their asynchronous log-only spill path.
|
|
119
|
+
if (exec.parent !== undefined && !hasImages)
|
|
120
|
+
return decision;
|
|
121
|
+
const retained = await bound(exec, content, exec.name, exec.callId, exec.parent === undefined ? 'result' : 'dispatch');
|
|
122
|
+
if (retained === undefined)
|
|
123
|
+
return decision;
|
|
124
|
+
const additionalContexts = [...decision.additionalContexts ?? []];
|
|
125
|
+
if (exec.parent !== undefined && !result.isError && hasImages && !retained.some(block => block.type === 'image')) {
|
|
126
|
+
additionalContexts.push(createUserMessage({ content: retained, source: { kind: 'ptc-mode' } }));
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
kind: 'accept', content: retained,
|
|
130
|
+
...additionalContexts.length > 0 ? { additionalContexts } : {},
|
|
131
|
+
};
|
|
132
|
+
}, { prepend: true });
|
|
133
|
+
ctx.on('tools/ptc-dispatch-log', async (dispatch, next) => {
|
|
134
|
+
const content = await next();
|
|
135
|
+
return await bound(dispatch.exec, content, dispatch.name, dispatch.subCallId, 'dispatch') ?? content;
|
|
136
|
+
}, { prepend: true });
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Browser-safe formatting and recognition of persisted spill-policy notices. */
|
|
2
|
+
import { type Omitted } from '@qlzt/dsh-output-retention';
|
|
3
|
+
import type { SpillRef } from '@qlzt/dsh-spill';
|
|
4
|
+
/**
|
|
5
|
+
* Format the notice appended to a retained preview, preserving its persisted spelling.
|
|
6
|
+
* @param omitted - bytes omitted by the retention policy.
|
|
7
|
+
* @param ref - saved text locator and retrieval guidance.
|
|
8
|
+
* @param images - number of whole images omitted alongside text.
|
|
9
|
+
* @returns the complete notice without a leading preview separator.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatSpillNotice(omitted: Omitted, ref: Pick<SpillRef, 'locator' | 'retrievalHint'>, images?: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* Recognize a final spill-policy notice in persisted text, including notice-only output.
|
|
14
|
+
* This identifies the text convention, not authenticated tool-output origin.
|
|
15
|
+
* @param text - complete recorded text result.
|
|
16
|
+
* @returns whether a complete notice occupies the end of the result.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasSpillNotice(text: string): boolean;
|
|
19
|
+
//# sourceMappingURL=notice.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Browser-safe formatting and recognition of persisted spill-policy notices. */
|
|
2
|
+
import { describeOmitted } from '@qlzt/dsh-output-retention';
|
|
3
|
+
const OPEN = '(';
|
|
4
|
+
const CLOSE = ')';
|
|
5
|
+
const LOCATION = ' Full formatted result stored at: ';
|
|
6
|
+
const GUIDANCE_SEPARATOR = '. ';
|
|
7
|
+
const SEPARATOR = '\n\n';
|
|
8
|
+
const EXACT_OMISSION = describeOmitted({ kind: 'exact', count: 0 }, 'bytes');
|
|
9
|
+
const COUNT_OFFSET = EXACT_OMISSION.indexOf('0');
|
|
10
|
+
const COUNT_SUFFIX = EXACT_OMISSION.slice(COUNT_OFFSET + 1);
|
|
11
|
+
/**
|
|
12
|
+
* Format the notice appended to a retained preview, preserving its persisted spelling.
|
|
13
|
+
* @param omitted - bytes omitted by the retention policy.
|
|
14
|
+
* @param ref - saved text locator and retrieval guidance.
|
|
15
|
+
* @param images - number of whole images omitted alongside text.
|
|
16
|
+
* @returns the complete notice without a leading preview separator.
|
|
17
|
+
*/
|
|
18
|
+
export function formatSpillNotice(omitted, ref, images = 0) {
|
|
19
|
+
const imageNotice = images > 0 ? ` Omitted ${images} images.` : '';
|
|
20
|
+
return `${OPEN}${describeOmitted(omitted, 'bytes')}${imageNotice}${LOCATION}${ref.locator}${GUIDANCE_SEPARATOR}${ref.retrievalHint}${CLOSE}`;
|
|
21
|
+
}
|
|
22
|
+
function isOmission(text) {
|
|
23
|
+
const imageNotice = / Omitted ([1-9][0-9]*) images\.$/.exec(text);
|
|
24
|
+
if (imageNotice !== null) {
|
|
25
|
+
if (!Number.isSafeInteger(Number(imageNotice[1])))
|
|
26
|
+
return false;
|
|
27
|
+
text = text.slice(0, imageNotice.index);
|
|
28
|
+
}
|
|
29
|
+
if (text === describeOmitted({ kind: 'none' }, 'bytes')
|
|
30
|
+
|| text === describeOmitted({ kind: 'unknown' }, 'bytes'))
|
|
31
|
+
return true;
|
|
32
|
+
const count = Number(text.slice(COUNT_OFFSET, text.length - COUNT_SUFFIX.length));
|
|
33
|
+
return Number.isSafeInteger(count) && count >= 0
|
|
34
|
+
&& text === describeOmitted({ kind: 'exact', count }, 'bytes');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Recognize a final spill-policy notice in persisted text, including notice-only output.
|
|
38
|
+
* This identifies the text convention, not authenticated tool-output origin.
|
|
39
|
+
* @param text - complete recorded text result.
|
|
40
|
+
* @returns whether a complete notice occupies the end of the result.
|
|
41
|
+
*/
|
|
42
|
+
export function hasSpillNotice(text) {
|
|
43
|
+
if (!text.endsWith(CLOSE))
|
|
44
|
+
return false;
|
|
45
|
+
let start = 0;
|
|
46
|
+
while (true) {
|
|
47
|
+
const next = text.indexOf(`${SEPARATOR}${OPEN}`, start);
|
|
48
|
+
const candidate = text.slice(start, next < 0 ? -CLOSE.length : next);
|
|
49
|
+
const location = candidate.indexOf(LOCATION, OPEN.length);
|
|
50
|
+
if (candidate.startsWith(OPEN) && location >= 0
|
|
51
|
+
&& isOmission(candidate.slice(OPEN.length, location))) {
|
|
52
|
+
return text.indexOf(GUIDANCE_SEPARATOR, start + location + LOCATION.length) >= 0;
|
|
53
|
+
}
|
|
54
|
+
if (next < 0)
|
|
55
|
+
return false;
|
|
56
|
+
start = next + SEPARATOR.length;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=notice.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Ordered head/tail retention of text and indivisible images. @module @qlzt/dsh-spill-policy/retention */
|
|
2
|
+
import type { ContentBlock } from '@qlzt/dsh-llm';
|
|
3
|
+
/** Content whose text can be split and whose images must remain whole. */
|
|
4
|
+
export type RetainableBlock = Extract<ContentBlock, {
|
|
5
|
+
type: 'text' | 'image';
|
|
6
|
+
}>;
|
|
7
|
+
/** Retained ends and exact quantities excluded from their original sequence. */
|
|
8
|
+
export interface RetainedContent {
|
|
9
|
+
head: RetainableBlock[];
|
|
10
|
+
tail: RetainableBlock[];
|
|
11
|
+
omittedBytes: number;
|
|
12
|
+
omittedImages: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Retain contiguous ends without moving or partially retaining an image.
|
|
16
|
+
* Each end receives half the content budget; unused space at an indivisible
|
|
17
|
+
* image stays unused. Text pricing must be monotonic in retained length.
|
|
18
|
+
* @param content - ordered text and image blocks whose total price exceeds budget.
|
|
19
|
+
* @param budget - non-negative integer token budget after reserving notices.
|
|
20
|
+
* @param price - model-route cost of one retained block, including its framing.
|
|
21
|
+
* @returns retained ends and omitted UTF-8 text bytes and whole images.
|
|
22
|
+
*/
|
|
23
|
+
export declare function retainContent(content: readonly RetainableBlock[], budget: number, price: (block: RetainableBlock) => number): RetainedContent;
|
|
24
|
+
//# sourceMappingURL=retention.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Preserve a UTF-16 code point when a text budget cuts between its surrogates. */
|
|
2
|
+
function textSlice(text, length, tail) {
|
|
3
|
+
let cut = tail ? text.length - length : length;
|
|
4
|
+
const previous = text.charCodeAt(cut - 1);
|
|
5
|
+
const current = text.charCodeAt(cut);
|
|
6
|
+
if (previous >= 0xd800 && previous <= 0xdbff && current >= 0xdc00 && current <= 0xdfff) {
|
|
7
|
+
cut += tail ? 1 : -1;
|
|
8
|
+
}
|
|
9
|
+
return tail ? text.slice(cut) : text.slice(0, cut);
|
|
10
|
+
}
|
|
11
|
+
/** Largest contiguous text end fitting a monotonic token estimate. */
|
|
12
|
+
function fitText(text, budget, tail, price) {
|
|
13
|
+
let low = 0;
|
|
14
|
+
let high = text.length;
|
|
15
|
+
while (low < high) {
|
|
16
|
+
const middle = Math.ceil((low + high) / 2);
|
|
17
|
+
const candidate = textSlice(text, middle, tail);
|
|
18
|
+
if (candidate.length === 0 || price({ type: 'text', text: candidate }) <= budget)
|
|
19
|
+
low = middle;
|
|
20
|
+
else
|
|
21
|
+
high = middle - 1;
|
|
22
|
+
}
|
|
23
|
+
return textSlice(text, low, tail);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Retain contiguous ends without moving or partially retaining an image.
|
|
27
|
+
* Each end receives half the content budget; unused space at an indivisible
|
|
28
|
+
* image stays unused. Text pricing must be monotonic in retained length.
|
|
29
|
+
* @param content - ordered text and image blocks whose total price exceeds budget.
|
|
30
|
+
* @param budget - non-negative integer token budget after reserving notices.
|
|
31
|
+
* @param price - model-route cost of one retained block, including its framing.
|
|
32
|
+
* @returns retained ends and omitted UTF-8 text bytes and whole images.
|
|
33
|
+
*/
|
|
34
|
+
export function retainContent(content, budget, price) {
|
|
35
|
+
const head = [];
|
|
36
|
+
const tail = [];
|
|
37
|
+
let first = 0;
|
|
38
|
+
let last = content.length - 1;
|
|
39
|
+
let headCharacters = 0;
|
|
40
|
+
let remaining = Math.ceil(budget / 2);
|
|
41
|
+
while (first <= last) {
|
|
42
|
+
const block = content[first];
|
|
43
|
+
const cost = price(block);
|
|
44
|
+
if (cost <= remaining) {
|
|
45
|
+
head.push(block);
|
|
46
|
+
remaining -= cost;
|
|
47
|
+
first++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (block.type === 'text') {
|
|
51
|
+
const text = fitText(block.text, remaining, false, price);
|
|
52
|
+
headCharacters = text.length;
|
|
53
|
+
if (text.length > 0)
|
|
54
|
+
head.push({ type: 'text', text });
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
remaining = Math.floor(budget / 2);
|
|
59
|
+
while (last >= first) {
|
|
60
|
+
const original = content[last];
|
|
61
|
+
const block = last === first && original.type === 'text'
|
|
62
|
+
? { type: 'text', text: original.text.slice(headCharacters) }
|
|
63
|
+
: original;
|
|
64
|
+
const cost = price(block);
|
|
65
|
+
if (cost <= remaining) {
|
|
66
|
+
tail.push(block);
|
|
67
|
+
remaining -= cost;
|
|
68
|
+
last--;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (block.type === 'text') {
|
|
72
|
+
const text = fitText(block.text, remaining, true, price);
|
|
73
|
+
if (text.length > 0)
|
|
74
|
+
tail.push({ type: 'text', text });
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
tail.reverse();
|
|
79
|
+
const bytes = (blocks) => blocks.reduce((total, block) => total + (block.type === 'text' ? Buffer.byteLength(block.text, 'utf8') : 0), 0);
|
|
80
|
+
const images = (blocks) => blocks.filter(block => block.type === 'image').length;
|
|
81
|
+
return {
|
|
82
|
+
head,
|
|
83
|
+
tail,
|
|
84
|
+
omittedBytes: bytes(content) - bytes(head) - bytes(tail),
|
|
85
|
+
omittedImages: images(content) - images(head) - images(tail),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary for the spill-policy plugin: the minimal structural view of a tool
|
|
3
|
+
* execution the policy needs to derive the owning session for a spill artifact.
|
|
4
|
+
*
|
|
5
|
+
* `@qlzt/dsh-tools`' `ToolExecution` satisfies this shape, so the policy
|
|
6
|
+
* reads `exec` straight through without importing `dsh-tools` or `dsh-agent`.
|
|
7
|
+
* Only the session HEADER id is read — the same identity every other subsystem
|
|
8
|
+
* keys off (see `dsh-tool-bash`'s owner derivation).
|
|
9
|
+
*
|
|
10
|
+
* @module @qlzt/dsh-spill-policy/types
|
|
11
|
+
*/
|
|
12
|
+
import type { SessionId } from '@qlzt/dsh-session';
|
|
13
|
+
/** Minimal structural view of a tool execution: the owning session's header id, when present. */
|
|
14
|
+
export interface SpillPolicyExec {
|
|
15
|
+
/** The agent on whose behalf the call runs, when there is one. */
|
|
16
|
+
agent?: {
|
|
17
|
+
session: {
|
|
18
|
+
header: {
|
|
19
|
+
/** The canonical session identity — the spill owner. */
|
|
20
|
+
id: SessionId;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vocabulary for the spill-policy plugin: the minimal structural view of a tool
|
|
3
|
+
* execution the policy needs to derive the owning session for a spill artifact.
|
|
4
|
+
*
|
|
5
|
+
* `@qlzt/dsh-tools`' `ToolExecution` satisfies this shape, so the policy
|
|
6
|
+
* reads `exec` straight through without importing `dsh-tools` or `dsh-agent`.
|
|
7
|
+
* Only the session HEADER id is read — the same identity every other subsystem
|
|
8
|
+
* keys off (see `dsh-tool-bash`'s owner derivation).
|
|
9
|
+
*
|
|
10
|
+
* @module @qlzt/dsh-spill-policy/types
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qlzt/dsh-spill-policy",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "Token-budgeted tool-result retention with recoverable text and image paths",
|
|
4
|
+
"version": "0.2.1-alpha.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://gitee.com/zsping1989/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/spill/spill-policy"
|
|
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
|
+
"./notice": {
|
|
22
|
+
"types": "./lib/types/notice.d.ts",
|
|
23
|
+
"default": "./lib/types/notice.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/types/**/*.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@qlzt/dsh-llm": "0.2.1-alpha.3",
|
|
36
|
+
"@qlzt/dsh-output-retention": "0.2.1-alpha.3",
|
|
37
|
+
"@qlzt/dsh-session": "0.2.1-alpha.3",
|
|
38
|
+
"@qlzt/dsh-spill": "0.2.1-alpha.3",
|
|
39
|
+
"@qlzt/dsh-tools": "0.2.1-alpha.3",
|
|
40
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
|
|
41
|
+
"@qlzt/dsh-attachment": "0.2.1-alpha.3",
|
|
42
|
+
"@qlzt/dsh-fs": "0.2.1-alpha.3",
|
|
43
|
+
"@qlzt/dsh-token-meter": "0.2.1-alpha.3"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@deepseek-ai/schemastery": "~3.18.5-alpha.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.3",
|
|
50
|
+
"@deepseek-ai/dsh-ptc-runtime-node": "0.2.1-alpha.3",
|
|
51
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.3",
|
|
52
|
+
"@deepseek-ai/dsh-output-retention": "0.2.1-alpha.3",
|
|
53
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.3",
|
|
54
|
+
"@deepseek-ai/dsh-spill": "0.2.1-alpha.3",
|
|
55
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.3",
|
|
56
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1",
|
|
57
|
+
"@deepseek-ai/dsh-fs-local": "0.2.1-alpha.3",
|
|
58
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.1-alpha.3",
|
|
59
|
+
"@deepseek-ai/dsh-sandbox-local": "0.2.1-alpha.3",
|
|
60
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.3",
|
|
61
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.3",
|
|
62
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-alpha.3",
|
|
63
|
+
"@deepseek-ai/dsh-fs": "0.2.1-alpha.3",
|
|
64
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.3",
|
|
65
|
+
"@deepseek-ai/dsh-mcp-client": "0.2.1-alpha.3",
|
|
66
|
+
"@deepseek-ai/dsh-spill-local": "0.2.1-alpha.3",
|
|
67
|
+
"@deepseek-ai/dsh-llm-deepseek": "0.2.1-alpha.3",
|
|
68
|
+
"@deepseek-ai/dsh-ptc-runtime": "0.2.1-alpha.3",
|
|
69
|
+
"@deepseek-ai/dsh-attachment-local": "0.2.1-alpha.3",
|
|
70
|
+
"@deepseek-ai/dsh-tool-fs": "0.2.1-alpha.3",
|
|
71
|
+
"sharp": "^0.35.5",
|
|
72
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "0.2.1-alpha.3"
|
|
73
|
+
},
|
|
74
|
+
"dsh": {}
|
|
75
|
+
}
|