@deepseek-ai/dsh-spill-policy 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -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/spill/spill-policy/README.md
5
+ README.md: 7638f62c0426964d83d7a635ebf73d0f117abd78
6
+ README.zh.md: f3bb66d7a65edb6f4d2f2a0e86d0d83bb1a262ab
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @deepseek-ai/dsh-spill-policy
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ The **tool-result spill policy**: a `tools/post-execute` transformer that keeps oversized plain-text tool results out of the model's context. When a final result exceeds `maxInlineBytes`, it saves the FULL text through [`ctx.spillStore`](../spill) and replaces the model-facing result with a bounded head/tail preview plus the backend's locator and retrieval hint.
6
+
7
+ This plugin registers **no service** and owns no storage or preview mechanics: preview is [`@deepseek-ai/dsh-retention`](../../util/retention) (`TextRetainer`), storage is `ctx.spillStore`. It only decides WHEN to spill and composes the notice.
8
+
9
+ ## Config
10
+
11
+ | Key | Default | Meaning |
12
+ |---|---|---|
13
+ | `maxInlineBytes` | *(omitted)* | Model-facing context cap for a plain-text result, in UTF-8 bytes (a non-negative integer; validated at load). **Omitted disables the policy entirely** (the plugin registers nothing). When set, a larger result is spilled and replaced with a preview derived from the same budget (head/tail split). |
14
+
15
+ ## Behavior
16
+
17
+ 1. Let the tool run (delegates via `next()`, so it bounds whatever a downstream hook accepted).
18
+ 2. Skip nested executions (`exec.parent` is present — their DURABLE copy is bounded by the dispatch-log arm below), accepted value replacements (the registry must revalidate and rerender them), `read` (avoids a `read → spill → read again` loop), and any non-`accept` decision (a `block`'s corrective feedback passes through).
19
+ 3. Flatten the accepted content only when it is **plain text** (all `text` blocks); a result with any non-text block is left untouched.
20
+ 4. If its UTF-8 size is `≤ maxInlineBytes`, leave it unchanged.
21
+ 5. Otherwise save the full text and replace the result with a preview + this notice, sized so the whole replacement (preview + blank line + notice) stays within `maxInlineBytes` — the notice's byte cost is reserved out of the budget, so the preview shrinks to fit and the model-facing result never exceeds the cap:
22
+
23
+ ```text
24
+ <retained head/tail preview>
25
+
26
+ (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.)
27
+ ```
28
+
29
+ When the notice alone fills the budget (a tiny cap or a long locator) the preview is empty and only the notice is returned. If even that notice-only replacement would exceed `maxInlineBytes`, the policy keeps the inline result — it never emits a replacement over the cap (and a within-cap replacement is always smaller than the original, so this also means spilling never adds bytes).
30
+
31
+ **Best-effort:** no session owner, no `ctx.spillStore` backend, or a `saveText` rejection ⇒ the policy logs a warning and returns the original result. A spill failure never turns a successful call into an `isError` or hides the inline result. A successful replacement changes only `content`; the canonical programmatic value is preserved.
32
+
33
+ **The dispatch-log arm:** a second listener on `tools/code-dispatch-log` applies the same cap, replacement pipeline, and best-effort fallbacks to the DURABLE copy of each `run_code` sub-call result (artifact label `dispatch`, keyed by the sub-call id). The program's value is untouched — it already crossed the worker boundary whole — and `read` sub-calls are bounded too: a log copy is not model context, so the read-again loop cannot occur, and `read` is precisely the tool that produces huge logs ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)).
34
+
35
+ ## Scope
36
+
37
+ The policy sees only the FINAL formatted surface result—not a tool's internal resource or canonical value. If a provider already truncated (e.g. `web-fetch-local.maxBodyChars`), the spill artifact holds the full formatted result the tool returned, not the full original source. Provider/resource caps stay mandatory and separate. `glob`/`grep` own item-level surface spill because their complete acquired values still exist before rendering; bash streams own acquisition-time spill. The generic policy prepends its waterfall listener, then delegates, so ordinary tool-owned asynchronous projections complete before generic byte bounding regardless of plugin load order. See the [tool output spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md).
38
+
39
+ ## Model Experience
40
+
41
+ ### Oversized plain-text result
42
+
43
+ #### What the model sees
44
+
45
+ Results at or below `maxInlineBytes`, nested results, `read` results, blocked decisions, and results containing non-text blocks are unchanged. An oversized plain-text surface result becomes a bounded head/tail preview followed by `(Omitted <bytes> bytes. Full formatted result stored at: <locator>. <retrievalHint>)`; storage or ownership failures leave the original result visible.
46
+
47
+ #### Token effect
48
+
49
+ A successful replacement is at most `maxInlineBytes` UTF-8 bytes and remains in history until compaction; the full spill text is not resent to the model.
50
+
51
+ #### KV Cache effect
52
+
53
+ Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
54
+
55
+ ## Known Limitations and Deferred Work
56
+
57
+ - **Only final plain-text results are spillable** — mixed-content results, blocked feedback, and `read` pass through; provider truncation or tool-owned retention that happened earlier cannot be recovered here.
58
+ - **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.
package/README.zh.md ADDED
@@ -0,0 +1,58 @@
1
+ # @deepseek-ai/dsh-spill-policy
2
+
3
+ [English](README.md) | 中文
4
+
5
+ **工具结果 spill 策略**:一个 `tools/post-execute` 转换器,用于防止过大的纯文本工具结果进入模型上下文。当最终结果超过 `maxInlineBytes` 时,它会通过 [`ctx.spillStore`](../spill) 保存完整文本,并将面向模型的结果替换为有界的首尾预览、后端定位信息与取回指引。
6
+
7
+ 该插件**不注册任何服务**,也不负责存储或预览机制:预览由 [`@deepseek-ai/dsh-retention`](../../util/retention)(`TextRetainer`)负责,存储由 `ctx.spillStore` 负责。它只决定何时 spill,并组合通知。
8
+
9
+ ## 配置
10
+
11
+ | 键 | 默认值 | 含义 |
12
+ |---|---|---|
13
+ | `maxInlineBytes` | *(省略)* | 面向模型的纯文本结果上下文上限,以 UTF-8 字节数计(在加载时验证为非负整数)。**省略时完全禁用该策略**(插件不注册任何内容)。设置后,超过该上限的结果会被 spill,并替换为从同一预算派生的预览(首尾拆分)。 |
14
+
15
+ ## 行为
16
+
17
+ 1. 允许工具运行(通过 `next()` 委托,因此可以限制任何下游钩子接受的结果)。
18
+ 2. 跳过嵌套执行(存在 `exec.parent`——其持久化副本由下方的 dispatch-log 分支设界)、已接受的值替换(注册表必须重新验证并重新渲染它们)、`read`(避免 `read → spill → read again` 循环)以及任何非 `accept` 决策(`block` 的纠正反馈会原样通过)。
19
+ 3. 仅在已接受的内容为**纯文本**(全部都是 `text` 块)时才将其展平;包含任何非文本块的结果都保持不变。
20
+ 4. 如果 UTF-8 大小为 `≤ maxInlineBytes`,则保持不变。
21
+ 5. 否则,保存完整文本,并将结果替换为预览和以下通知。系统会调整大小,使整个替换内容(预览、空行和通知)不超过 `maxInlineBytes`:先从预算中保留通知所需字节,再缩小预览以适配剩余空间,因此面向模型的结果绝不会超过上限:
22
+
23
+ ```text
24
+ <retained head/tail preview>
25
+
26
+ (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.)
27
+ ```
28
+
29
+ 当通知本身已占满预算时(上限极小或定位信息很长),预览为空,只返回通知。如果仅通知的替换内容仍会超过 `maxInlineBytes`,策略将保留内联结果;它绝不会发出超过上限的替换内容(而且上限内的替换内容总比原结果更小,因此这也意味着 spill 绝不会增加字节数)。
30
+
31
+ **尽力而为**:没有会话所有者、没有 `ctx.spillStore` 后端,或 `saveText` 返回拒绝 ⇒ 策略记录警告并返回原始结果。spill 失败绝不会将成功调用变为 `isError`,也不会隐藏内联结果。成功替换时只会更改 `content`;规范的程序化值保持不变。
32
+
33
+ **dispatch-log 分支:**注册在 `tools/code-dispatch-log` 上的第二个监听器,把同一套上限、替换流水线与尽力而为的回退应用到每个 `run_code` 子调用结果的持久化副本上(产物标签为 `dispatch`,按子调用 id 归档)。程序的值不受影响,因为它早已完整跨过 worker 边界;`read` 子调用同样设界:日志副本不是模型上下文,因此不会发生 read-again 循环,而 `read` 恰恰是最容易产生巨型日志的工具([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
34
+
35
+ ## 范围
36
+
37
+ 该策略只能看到最终格式化的呈现结果,看不到工具的内部资源或规范值。如果提供方已经截断内容(例如 `web-fetch-local.maxBodyChars`),spill 产物保存的是工具返回的完整格式化结果,而非完整原始源。提供方/资源上限仍然是必需的,并且与该策略相互独立。`glob`/`grep` 负责对项级呈现结果执行 spill,因为渲染前仍然存在完整的已获取值;bash 流负责在获取时 spill。通用策略预先注册自己的 waterfall(瀑布式事件)监听器,然后再委托,因此无论插件加载顺序如何,普通工具自身的异步投影都会在通用字节限制之前完成。详见[工具输出 spill Agent Note](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md)。
38
+
39
+ ## 模型体验
40
+
41
+ ### 过大的纯文本结果
42
+
43
+ #### 模型看到的内容
44
+
45
+ 大小不超过 `maxInlineBytes` 的结果、嵌套结果、`read` 结果、被阻止的决策和包含非文本块的结果都保持不变。过大的纯文本呈现结果会变为有界的首尾预览,后面附加 `(Omitted <bytes> bytes. Full formatted result stored at: <locator>. <retrievalHint>)`;存储失败或没有会话所有者时,原始结果仍然可见。
46
+
47
+ #### Token 影响
48
+
49
+ 成功替换后的内容最多为 `maxInlineBytes` 个 UTF-8 字节,并会保留在历史中直到压缩(compaction);完整 spill 文本不会重新发送给模型。
50
+
51
+ #### KV Cache 影响
52
+
53
+ 仅追加;新可见内容位于可重用请求前缀之后,不会使现有 KV Cache 条目失效。
54
+
55
+ ## 已知限制与暂缓事项
56
+
57
+ - **只能对最终纯文本结果执行 spill**:混合内容结果、阻止反馈和 `read` 会原样通过;无法在此恢复先前已经发生的提供方截断或工具自身执行的保留处理。
58
+ - **通知无法容纳时,该次调用的替换功能会禁用**:当上限极小或定位信息很长时,后端已经保存了无引用的 spill,但过大的原始结果仍会保留在内联位置。
package/lib/index.js ADDED
@@ -0,0 +1,169 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { TextRetainer, describeOmitted } from "@deepseek-ai/dsh-retention";
3
+ //#region lib/types/index.js
4
+ /**
5
+ * The spill-policy PLUGIN: a `tools/post-execute` result transformer that keeps
6
+ * oversized plain-text tool results out of the model's context. When a final
7
+ * result's UTF-8 size exceeds `maxInlineBytes`, it saves the FULL text to a
8
+ * session-scoped spill artifact (`ctx.spillStore`) and replaces the
9
+ * model-facing result with a bounded head/tail preview plus the backend's
10
+ * locator and retrieval guidance.
11
+ *
12
+ * It registers NO service and owns NO storage or preview mechanics: preview is
13
+ * `@deepseek-ai/dsh-retention` (`TextRetainer`), storage is `ctx.spillStore`.
14
+ * The policy only decides WHEN to spill and composes the notice.
15
+ *
16
+ * A second arm applies the SAME cap to the durable log: the
17
+ * `tools/code-dispatch-log` waterfall bounds the `tool/code-dispatch` event's
18
+ * copy of an oversized `run_code` sub-call result (the program's value is
19
+ * untouched; UIs and replay read the full text through the spill artifact).
20
+ *
21
+ * ## Deliberately narrow
22
+ *
23
+ * - Omitted `maxInlineBytes` ⇒ the plugin registers nothing (a true no-op).
24
+ * - Plain-text results only: a result carrying any non-text block is left
25
+ * untouched (the policy knows only the final formatted text, not tool
26
+ * internals).
27
+ * - Nested composite calls skip the MODEL-facing arm; their durable log copy
28
+ * is bounded by the dispatch-log arm instead.
29
+ * - Accepted value replacements pass through for registry revalidation and
30
+ * rendering; this presentation policy cannot also replace content in the
31
+ * same mutually exclusive decision.
32
+ * - `read` is skipped by the model-facing arm to avoid a
33
+ * `read → spill → read again` loop; the dispatch-log arm bounds `read`
34
+ * sub-calls too (a log copy is not model context, and `read` is precisely
35
+ * the tool that produces huge logs).
36
+ * - Best-effort: no session owner, no `ctx.spillStore` backend, or a save
37
+ * failure ⇒ log and return the original result. A spill failure must NEVER
38
+ * turn a successful tool call into an `isError` or hide the inline result.
39
+ *
40
+ * It COMPOSES with other post-execute listeners: its prepended listener
41
+ * delegates via `next()` and bounds the resulting content projection, so
42
+ * tool-owned asynchronous projection runs before generic bounding, a hook that
43
+ * replaced content still has its replacement bounded, and value replacements
44
+ * and `block` decisions pass through unchanged.
45
+ *
46
+ * @module @deepseek-ai/dsh-spill-policy
47
+ */
48
+ /** Cordis plugin name used by loader diagnostics. */
49
+ const name = "spill-policy";
50
+ /** Require the tool registry (its `tools/post-execute` waterfall is the extension point we transform). */
51
+ const inject = ["tools"];
52
+ const Config = z.object({ maxInlineBytes: z.number() });
53
+ /** All-text content flattened to one UTF-8 string, or `undefined` if any block is non-text. */
54
+ function flattenPlainText(content) {
55
+ let text = "";
56
+ for (const block of content) {
57
+ if (block.type !== "text") return void 0;
58
+ text += block.text;
59
+ }
60
+ return text;
61
+ }
62
+ /** The owning session id, or `undefined` for a call with no agent (a direct/test call). */
63
+ function ownerSessionId(exec) {
64
+ return exec.agent?.session.header.id;
65
+ }
66
+ /** Build the bounded head/tail preview for `text`, splitting `budget` bytes across the two ends. */
67
+ function preview(text, budget) {
68
+ const retainer = new TextRetainer({
69
+ kind: "headTail",
70
+ headBytes: Math.ceil(budget / 2),
71
+ tailBytes: Math.floor(budget / 2)
72
+ });
73
+ retainer.push(text);
74
+ const kept = retainer.finish();
75
+ return {
76
+ text: kept.text,
77
+ omitted: kept.omittedBytes
78
+ };
79
+ }
80
+ /** The spill-notice line for a given omission + saved reference (no preview, no leading blank line). */
81
+ function spillNotice(omitted, ref) {
82
+ return `(${describeOmitted(omitted, "bytes")} Full formatted result stored at: ${ref.locator}. ${ref.retrievalHint})`;
83
+ }
84
+ function apply(ctx, config) {
85
+ const maxInlineBytes = config.maxInlineBytes;
86
+ if (maxInlineBytes === void 0) return;
87
+ if (!Number.isInteger(maxInlineBytes) || maxInlineBytes < 0) throw new Error(`spill-policy: maxInlineBytes must be a non-negative integer (got ${maxInlineBytes})`);
88
+ const cap = maxInlineBytes;
89
+ /**
90
+ * Spill `text` and build the bounded replacement (preview + notice), or
91
+ * return `undefined` when the policy must keep the original (no session
92
+ * owner, no backend, storage failure, or no within-cap replacement).
93
+ * Shared verbatim by the model-facing post-execute arm and the durable
94
+ * dispatch-log arm so both produce byte-identical projections.
95
+ */
96
+ async function spillReplacement(text, totalBytes, sessionId, toolName, callId, label) {
97
+ if (sessionId === void 0) {
98
+ ctx.logger.warn(`spill-policy: no session owner for ${toolName} ${label}; keeping the inline content`);
99
+ return;
100
+ }
101
+ const spillStore = ctx.get("spillStore");
102
+ if (!spillStore) {
103
+ ctx.logger.warn("spill-policy: no ctx.spillStore backend loaded; keeping the inline content");
104
+ return;
105
+ }
106
+ const save = {
107
+ owner: { sessionId },
108
+ source: {
109
+ toolName,
110
+ callId,
111
+ label
112
+ },
113
+ suggestedName: `${toolName}.txt`,
114
+ content: text
115
+ };
116
+ let ref;
117
+ try {
118
+ ref = await spillStore.saveText(save);
119
+ } catch (error) {
120
+ ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`);
121
+ return;
122
+ }
123
+ const reserve = Buffer.byteLength(spillNotice({
124
+ kind: "exact",
125
+ count: totalBytes
126
+ }, ref), "utf8") + 2;
127
+ const { text: previewText, omitted } = preview(text, Math.max(0, cap - reserve));
128
+ const notice = spillNotice(omitted, ref);
129
+ const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice;
130
+ if (Buffer.byteLength(replacedText, "utf8") > cap) {
131
+ ctx.logger.warn(`spill-policy: spill notice for ${toolName} exceeds maxInlineBytes; keeping the inline content`);
132
+ return;
133
+ }
134
+ return replacedText;
135
+ }
136
+ ctx.on("tools/post-execute", async (exec, result, next) => {
137
+ const decision = await next();
138
+ if (decision.kind !== "accept" || Object.hasOwn(decision, "value") || exec.parent !== void 0 || exec.name === "read") return decision;
139
+ const text = flattenPlainText(decision.content ?? result.content);
140
+ if (text === void 0) return decision;
141
+ const totalBytes = Buffer.byteLength(text, "utf8");
142
+ if (totalBytes <= maxInlineBytes) return decision;
143
+ const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(exec), exec.name, exec.callId, "result");
144
+ if (replacedText === void 0) return decision;
145
+ return {
146
+ kind: "accept",
147
+ content: [{
148
+ type: "text",
149
+ text: replacedText
150
+ }],
151
+ ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {}
152
+ };
153
+ }, { prepend: true });
154
+ ctx.on("tools/code-dispatch-log", async (dispatch, next) => {
155
+ const content = await next();
156
+ const text = flattenPlainText(content);
157
+ if (text === void 0) return content;
158
+ const totalBytes = Buffer.byteLength(text, "utf8");
159
+ if (totalBytes <= maxInlineBytes) return content;
160
+ const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(dispatch.exec), dispatch.name, dispatch.subCallId, "dispatch");
161
+ if (replacedText === void 0) return content;
162
+ return [{
163
+ type: "text",
164
+ text: replacedText
165
+ }];
166
+ }, { prepend: true });
167
+ }
168
+ //#endregion
169
+ export { Config, apply, inject, name };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-spill-policy`.
4
+ * @module @deepseek-ai/dsh-spill-policy/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-spill-policy";
7
+ /** Cordis companion plugin name. */
8
+ const name = "spill-policy-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: this package exposes no independent event sequence or mutable data relation
13
+ * beyond contracts enforced at its owning seam.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The spill-policy PLUGIN: a `tools/post-execute` result transformer that keeps
3
+ * oversized plain-text tool results out of the model's context. When a final
4
+ * result's UTF-8 size exceeds `maxInlineBytes`, it saves the FULL text to a
5
+ * session-scoped spill artifact (`ctx.spillStore`) and replaces the
6
+ * model-facing result with a bounded head/tail preview plus the backend's
7
+ * locator and retrieval guidance.
8
+ *
9
+ * It registers NO service and owns NO storage or preview mechanics: preview is
10
+ * `@deepseek-ai/dsh-retention` (`TextRetainer`), storage is `ctx.spillStore`.
11
+ * The policy only decides WHEN to spill and composes the notice.
12
+ *
13
+ * A second arm applies the SAME cap to the durable log: the
14
+ * `tools/code-dispatch-log` waterfall bounds the `tool/code-dispatch` event's
15
+ * copy of an oversized `run_code` sub-call result (the program's value is
16
+ * untouched; UIs and replay read the full text through the spill artifact).
17
+ *
18
+ * ## Deliberately narrow
19
+ *
20
+ * - Omitted `maxInlineBytes` ⇒ the plugin registers nothing (a true no-op).
21
+ * - Plain-text results only: a result carrying any non-text block is left
22
+ * untouched (the policy knows only the final formatted text, not tool
23
+ * internals).
24
+ * - Nested composite calls skip the MODEL-facing arm; their durable log copy
25
+ * is bounded by the dispatch-log arm instead.
26
+ * - Accepted value replacements pass through for registry revalidation and
27
+ * rendering; this presentation policy cannot also replace content in the
28
+ * same mutually exclusive decision.
29
+ * - `read` is skipped by the model-facing arm to avoid a
30
+ * `read → spill → read again` loop; the dispatch-log arm bounds `read`
31
+ * sub-calls too (a log copy is not model context, and `read` is precisely
32
+ * the tool that produces huge logs).
33
+ * - Best-effort: no session owner, no `ctx.spillStore` backend, or a save
34
+ * failure ⇒ log and return the original result. A spill failure must NEVER
35
+ * turn a successful tool call into an `isError` or hide the inline result.
36
+ *
37
+ * It COMPOSES with other post-execute listeners: its prepended listener
38
+ * delegates via `next()` and bounds the resulting content projection, so
39
+ * tool-owned asynchronous projection runs before generic bounding, a hook that
40
+ * replaced content still has its replacement bounded, and value replacements
41
+ * and `block` decisions pass through unchanged.
42
+ *
43
+ * @module @deepseek-ai/dsh-spill-policy
44
+ */
45
+ import type { Context } from '@deepseek-ai/cordis';
46
+ import z from '@deepseek-ai/schemastery';
47
+ export type { SpillPolicyExec } from './types.ts';
48
+ /** Plugin config. */
49
+ export interface Config {
50
+ /**
51
+ * The model-facing context cap for a plain-text tool result, in UTF-8 bytes.
52
+ * Omitted disables the policy entirely (no-op). When set, a result larger than
53
+ * this is spilled and replaced with a preview derived from this same budget.
54
+ */
55
+ maxInlineBytes?: number;
56
+ }
57
+ /** Cordis plugin name used by loader diagnostics. */
58
+ export declare const name = "spill-policy";
59
+ /** Require the tool registry (its `tools/post-execute` waterfall is the extension point we transform). */
60
+ export declare const inject: string[];
61
+ export declare const Config: z<Config>;
62
+ export declare function apply(ctx: Context, config: Config): void;
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-spill-policy`.
3
+ * @module @deepseek-ai/dsh-spill-policy/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "spill-policy-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.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
+ * `@deepseek-ai/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 @deepseek-ai/dsh-spill-policy/types
11
+ */
12
+ import type { SessionId } from '@deepseek-ai/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
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-spill-policy",
3
+ "description": "Tool-result spill policy for the DeepSeek Harness — replaces oversized plain-text tool results with a retained preview plus a spill-file path (no service surface)",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/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
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./src/*": "./src/*",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "lib/index.js",
30
+ "lib/invariant.js",
31
+ "lib/types/**/*.d.ts"
32
+ ],
33
+ "license": "BSD-3-Clause",
34
+ "peerDependencies": {
35
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-retention": "^0.0.1-rc.1",
38
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
39
+ "@deepseek-ai/dsh-spill": "^0.0.1-rc.1",
40
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
41
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
42
+ },
43
+ "dependencies": {
44
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1"
45
+ },
46
+ "devDependencies": {
47
+ "@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
48
+ "@deepseek-ai/dsh-code-runtime-worker": "^0.0.1-rc.1",
49
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
50
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
51
+ "@deepseek-ai/dsh-retention": "^0.0.1-rc.1",
52
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
53
+ "@deepseek-ai/dsh-spill": "^0.0.1-rc.1",
54
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
55
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
56
+ }
57
+ }