@deepseek-ai/dsh-spill-policy 0.1.2-alpha.5 → 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +7 -1
- package/README.zh.md +7 -1
- package/lib/index.js +25 -6
- package/lib/types/index.js +195 -0
- package/lib/types/notice.d.ts +18 -0
- package/lib/types/notice.js +51 -0
- package/lib/types/types.js +13 -0
- package/package.json +18 -13
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/spill/spill-policy/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 1ccf250bacd0b896e6f140393fd3447de3af1ab0
|
|
6
|
+
README.zh.md: e6c537a1c9bd0c24a09063898638a12bfe84466d
|
package/README.md
CHANGED
|
@@ -86,11 +86,17 @@ The policy is deliberately narrow: it only decides **when** to spill and compose
|
|
|
86
86
|
|
|
87
87
|
A `tools/post-execute` waterfall listener (registered with `prepend`, delegating via `next()`) bounds the model-facing result; a `tools/ptc-dispatch-log` listener bounds the durable log copy of each `run_code` sub-call. Both share one replacement helper so the two projections are byte-identical. The post-execute arm skips `read` to avoid a read → spill → read loop; the dispatch-log arm bounds `read` sub-calls because a log copy is not model context.
|
|
88
88
|
|
|
89
|
+
<a id="shared-notice-ownership"></a>
|
|
90
|
+
### Shared notice ownership
|
|
91
|
+
|
|
92
|
+
The browser-safe `@deepseek-ai/dsh-spill-policy/notice` entry owns both `formatSpillNotice(omitted, ref)`, used by the producer, and `hasSpillNotice(text)`, used by presentation consumers. Formatting and recognition share the notice delimiters; omission validation uses the existing `describeOmitted` formatter rather than a second copy of its prose. Recognition accepts a complete final notice after a preview or by itself and preserves the persisted notice spelling. It reads recorded text without rewriting it.
|
|
93
|
+
|
|
89
94
|
### Source map
|
|
90
95
|
|
|
91
96
|
| File | Role |
|
|
92
97
|
|---|---|
|
|
93
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` |
|
|
94
100
|
| [`src/types.ts`](src/types.ts) | `SpillPolicyExec`: the minimal structural view of a tool execution the policy reads for the owning session id |
|
|
95
101
|
| — | No runtime invariant companion is published; this package exposes no independent event sequence or mutable data relation beyond contracts enforced at its owning seam. |
|
|
96
102
|
|
|
@@ -111,7 +117,6 @@ Read these pages when the package-level contract is not enough.
|
|
|
111
117
|
- [dsh-spill-local](../spill-local/README.md) — the local backend that stores the spilled text.
|
|
112
118
|
- [dsh-output-retention](../../util/output-retention/README.md) — the preview mechanics (`TextRetainer`) the policy composes.
|
|
113
119
|
- [Tool output spill decision](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.md) — the capability boundary and design rationale.
|
|
114
|
-
- [PTC dispatch-log spill decision](../../../.agents/notes/implemented/feature/2026-07-26-ptc-dispatch-log-spill.md) — why the durable log copy is bounded too.
|
|
115
120
|
|
|
116
121
|
-----
|
|
117
122
|
|
|
@@ -139,6 +144,7 @@ Append-only; newly visible content follows the reusable request prefix and does
|
|
|
139
144
|
|
|
140
145
|
These limits define when the policy cannot help. They are current package constraints.
|
|
141
146
|
|
|
147
|
+
- **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.
|
|
142
148
|
- **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.
|
|
143
149
|
- **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.
|
|
144
150
|
|
package/README.zh.md
CHANGED
|
@@ -86,11 +86,17 @@ kind: "package-reference"
|
|
|
86
86
|
|
|
87
87
|
`tools/post-execute` waterfall(瀑布式事件)监听器(以 `prepend` 注册、通过 `next()` 委托)约束面向模型的结果;`tools/ptc-dispatch-log` 监听器约束每个 `run_code` 子调用的持久日志副本。两者共享同一个替换辅助函数,因此两个投影字节一致。post-execute 分支跳过 `read` 以避免 read → spill → read 循环;dispatch-log 分支约束 `read` 子调用,因为日志副本不是模型上下文。
|
|
88
88
|
|
|
89
|
+
<a id="shared-notice-ownership"></a>
|
|
90
|
+
### 共享通知的所有权
|
|
91
|
+
|
|
92
|
+
浏览器安全入口 `@deepseek-ai/dsh-spill-policy/notice` 同时负责生产方使用的 `formatSpillNotice(omitted, ref)` 和展示消费方使用的 `hasSpillNotice(text)`。格式化与识别共用通知分隔符;省略信息通过现有的 `describeOmitted` 格式化函数校验,而非复制一套文案。识别支持预览之后或单独出现的完整末尾通知,并保留持久化通知的原有拼写。它只读取已记录的文本,不改写文本。
|
|
93
|
+
|
|
89
94
|
### 源码地图
|
|
90
95
|
|
|
91
96
|
| 文件 | 职责 |
|
|
92
97
|
|---|---|
|
|
93
98
|
| [`src/index.ts`](src/index.ts) | 插件入口:`Config` 校验、两个 waterfall 监听器、共享替换辅助函数 |
|
|
99
|
+
| [`src/notice.ts`](src/notice.ts) | 浏览器安全的通知格式化与识别,以 `./notice` 发布 |
|
|
94
100
|
| [`src/types.ts`](src/types.ts) | `SpillPolicyExec`:策略读取所属会话 id 所需的最小结构化工具执行视图 |
|
|
95
101
|
| — | 不发布运行时不变式伴生入口;约定在 seam 处强制执行。 |
|
|
96
102
|
|
|
@@ -111,7 +117,6 @@ kind: "package-reference"
|
|
|
111
117
|
- [dsh-spill-local](../spill-local/README.zh.md)——保存 spill 文本的本地后端。
|
|
112
118
|
- [dsh-output-retention](../../util/output-retention/README.zh.md)——策略组合的预览机制(`TextRetainer`)。
|
|
113
119
|
- [工具输出 spill 决策](../../../.agents/notes/implemented/architecture/2026-07-08-tool-output-spill-files.zh.md)——能力边界与设计依据。
|
|
114
|
-
- [PTC dispatch-log spill 决策](../../../.agents/notes/implemented/feature/2026-07-26-ptc-dispatch-log-spill.zh.md)——为何持久日志副本同样设界。
|
|
115
120
|
|
|
116
121
|
-----
|
|
117
122
|
|
|
@@ -139,6 +144,7 @@ kind: "package-reference"
|
|
|
139
144
|
|
|
140
145
|
这些限制说明策略在哪些情况下无法提供帮助。它们是当前的包约束。
|
|
141
146
|
|
|
147
|
+
- **文本识别无法认证输出来源**——工具也能打印相同的通知文本;`hasSpillNotice` 识别的是文本约定,不能证明策略保存过结果。
|
|
142
148
|
- **只能对最终纯文本结果执行 spill**——混合内容结果、阻止反馈与 `read` 会原样通过;此前已经发生的提供方截断或工具自有保留无法在此恢复。
|
|
143
149
|
- **通知无法容纳时会禁用该次调用的替换**——上限极小或定位信息很长时,后端已经保存了无引用的 spill,但过大的原始结果仍留在内联位置。
|
|
144
150
|
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
2
|
import { TextRetainer, describeOmitted } from "@deepseek-ai/dsh-output-retention";
|
|
3
|
+
//#region lib/types/notice.js
|
|
4
|
+
/** Browser-safe formatting and recognition of persisted spill-policy notices. */
|
|
5
|
+
const OPEN = "(";
|
|
6
|
+
const CLOSE = ")";
|
|
7
|
+
const LOCATION = " Full formatted result stored at: ";
|
|
8
|
+
const GUIDANCE_SEPARATOR = ". ";
|
|
9
|
+
const EXACT_OMISSION = describeOmitted({
|
|
10
|
+
kind: "exact",
|
|
11
|
+
count: 0
|
|
12
|
+
}, "bytes");
|
|
13
|
+
const COUNT_OFFSET = EXACT_OMISSION.indexOf("0");
|
|
14
|
+
EXACT_OMISSION.slice(COUNT_OFFSET + 1);
|
|
15
|
+
/**
|
|
16
|
+
* Format the notice appended to a retained preview, preserving its persisted spelling.
|
|
17
|
+
* @param omitted - bytes omitted by the retention policy.
|
|
18
|
+
* @param ref - saved text locator and retrieval guidance.
|
|
19
|
+
* @returns the complete notice without a leading preview separator.
|
|
20
|
+
*/
|
|
21
|
+
function formatSpillNotice(omitted, ref) {
|
|
22
|
+
return `${OPEN}${describeOmitted(omitted, "bytes")}${LOCATION}${ref.locator}${GUIDANCE_SEPARATOR}${ref.retrievalHint}${CLOSE}`;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
3
25
|
//#region lib/types/index.js
|
|
4
26
|
/**
|
|
5
27
|
* The spill-policy PLUGIN: a `tools/post-execute` result transformer that keeps
|
|
@@ -77,10 +99,6 @@ function preview(text, budget) {
|
|
|
77
99
|
omitted: kept.omittedBytes
|
|
78
100
|
};
|
|
79
101
|
}
|
|
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
102
|
function apply(ctx, config) {
|
|
85
103
|
const maxInlineBytes = config.maxInlineBytes;
|
|
86
104
|
if (maxInlineBytes === void 0) return;
|
|
@@ -106,6 +124,7 @@ function apply(ctx, config) {
|
|
|
106
124
|
const save = {
|
|
107
125
|
owner: { sessionId },
|
|
108
126
|
source: {
|
|
127
|
+
kind: "tool",
|
|
109
128
|
toolName,
|
|
110
129
|
callId,
|
|
111
130
|
label
|
|
@@ -120,12 +139,12 @@ function apply(ctx, config) {
|
|
|
120
139
|
ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`);
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
|
-
const reserve = Buffer.byteLength(
|
|
142
|
+
const reserve = Buffer.byteLength(formatSpillNotice({
|
|
124
143
|
kind: "exact",
|
|
125
144
|
count: totalBytes
|
|
126
145
|
}, ref), "utf8") + 2;
|
|
127
146
|
const { text: previewText, omitted } = preview(text, Math.max(0, cap - reserve));
|
|
128
|
-
const notice =
|
|
147
|
+
const notice = formatSpillNotice(omitted, ref);
|
|
129
148
|
const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice;
|
|
130
149
|
if (Buffer.byteLength(replacedText, "utf8") > cap) {
|
|
131
150
|
ctx.logger.warn(`spill-policy: spill notice for ${toolName} exceeds maxInlineBytes; keeping the inline content`);
|
|
@@ -0,0 +1,195 @@
|
|
|
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-output-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/ptc-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 z from '@deepseek-ai/schemastery';
|
|
46
|
+
import { TextRetainer } from '@deepseek-ai/dsh-output-retention';
|
|
47
|
+
import { formatSpillNotice } from "./notice.js";
|
|
48
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
49
|
+
export const name = 'spill-policy';
|
|
50
|
+
/** Require the tool registry (its `tools/post-execute` waterfall is the extension point we transform). */
|
|
51
|
+
export const inject = ['tools'];
|
|
52
|
+
export const Config = z.object({
|
|
53
|
+
maxInlineBytes: z.number(),
|
|
54
|
+
});
|
|
55
|
+
/** All-text content flattened to one UTF-8 string, or `undefined` if any block is non-text. */
|
|
56
|
+
function flattenPlainText(content) {
|
|
57
|
+
let text = '';
|
|
58
|
+
for (const block of content) {
|
|
59
|
+
if (block.type !== 'text')
|
|
60
|
+
return undefined;
|
|
61
|
+
text += block.text;
|
|
62
|
+
}
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
/** The owning session id, or `undefined` for a call with no agent (a direct/test call). */
|
|
66
|
+
function ownerSessionId(exec) {
|
|
67
|
+
return exec.agent?.session.header.id;
|
|
68
|
+
}
|
|
69
|
+
/** Build the bounded head/tail preview for `text`, splitting `budget` bytes across the two ends. */
|
|
70
|
+
function preview(text, budget) {
|
|
71
|
+
const headBytes = Math.ceil(budget / 2);
|
|
72
|
+
const tailBytes = Math.floor(budget / 2);
|
|
73
|
+
const retainer = new TextRetainer({ kind: 'headTail', headBytes, tailBytes });
|
|
74
|
+
retainer.push(text);
|
|
75
|
+
const kept = retainer.finish();
|
|
76
|
+
return { text: kept.text, omitted: kept.omittedBytes };
|
|
77
|
+
}
|
|
78
|
+
export function apply(ctx, config) {
|
|
79
|
+
const maxInlineBytes = config.maxInlineBytes;
|
|
80
|
+
// Omitted ⇒ no automatic spill policy: register nothing at all.
|
|
81
|
+
if (maxInlineBytes === undefined)
|
|
82
|
+
return;
|
|
83
|
+
// Validate at LOAD, not per call: a negative/fractional cap would reach
|
|
84
|
+
// TextRetainer's assertBudget and throw, turning every oversized-result call
|
|
85
|
+
// into an isError. A bad config must fail the deployment, not the tool.
|
|
86
|
+
if (!Number.isInteger(maxInlineBytes) || maxInlineBytes < 0) {
|
|
87
|
+
throw new Error(`spill-policy: maxInlineBytes must be a non-negative integer (got ${maxInlineBytes})`);
|
|
88
|
+
}
|
|
89
|
+
// Narrowed once for the nested arms (closure narrowing does not survive awaits).
|
|
90
|
+
const cap = maxInlineBytes;
|
|
91
|
+
/**
|
|
92
|
+
* Spill `text` and build the bounded replacement (preview + notice), or
|
|
93
|
+
* return `undefined` when the policy must keep the original (no session
|
|
94
|
+
* owner, no backend, storage failure, or no within-cap replacement).
|
|
95
|
+
* Shared verbatim by the model-facing post-execute arm and the durable
|
|
96
|
+
* dispatch-log arm so both produce byte-identical projections.
|
|
97
|
+
*/
|
|
98
|
+
async function spillReplacement(text, totalBytes, sessionId, toolName, callId, label) {
|
|
99
|
+
if (sessionId === undefined) {
|
|
100
|
+
ctx.logger.warn(`spill-policy: no session owner for ${toolName} ${label}; keeping the inline content`);
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const spillStore = ctx.get('spillStore');
|
|
104
|
+
if (!spillStore) {
|
|
105
|
+
ctx.logger.warn('spill-policy: no ctx.spillStore backend loaded; keeping the inline content');
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const save = {
|
|
109
|
+
owner: { sessionId },
|
|
110
|
+
source: { kind: 'tool', toolName, callId, label },
|
|
111
|
+
suggestedName: `${toolName}.txt`,
|
|
112
|
+
content: text,
|
|
113
|
+
};
|
|
114
|
+
let ref;
|
|
115
|
+
try {
|
|
116
|
+
ref = await spillStore.saveText(save);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// Best-effort: a storage failure (permissions, ENOSPC, backend down) must
|
|
120
|
+
// never fail the call or hide the content — keep the original inline.
|
|
121
|
+
ctx.logger.warn(`spill-policy: saveText failed for ${toolName}: ${String(error)}; keeping the inline content`);
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
// Reserve the notice's byte cost INSIDE maxInlineBytes so the replacement
|
|
125
|
+
// (preview + blank line + notice) never exceeds the documented cap — a naive
|
|
126
|
+
// preview that spent the whole budget then appended the notice could be
|
|
127
|
+
// larger than the cap, and for a marginally-over result even larger than the
|
|
128
|
+
// original. The reservation uses a notice priced at the worst-case omission
|
|
129
|
+
// count (the full byte total): its digit count bounds the real count's, so
|
|
130
|
+
// the reserved size is a safe upper bound and the final notice is never
|
|
131
|
+
// longer than what we reserved. `\n\n` is the 2-byte join.
|
|
132
|
+
const reserve = Buffer.byteLength(formatSpillNotice({ kind: 'exact', count: totalBytes }, ref), 'utf8') + 2;
|
|
133
|
+
const previewBudget = Math.max(0, cap - reserve);
|
|
134
|
+
const { text: previewText, omitted } = preview(text, previewBudget);
|
|
135
|
+
const notice = formatSpillNotice(omitted, ref);
|
|
136
|
+
const replacedText = previewText.length > 0 ? `${previewText}\n\n${notice}` : notice;
|
|
137
|
+
// Invariant: the policy NEVER emits a replacement larger than the cap. When
|
|
138
|
+
// the notice alone exceeds maxInlineBytes (a tiny cap or a long spill root),
|
|
139
|
+
// there is no within-cap replacement, so keep the inline content — spilling
|
|
140
|
+
// would break the advertised cap. (A within-cap replacement is always
|
|
141
|
+
// smaller than the original, which is > cap by the entry condition, so this
|
|
142
|
+
// one check subsumes "not smaller than the original" too. The spill file
|
|
143
|
+
// already written is a harmless orphan; cleanup is deferred.)
|
|
144
|
+
if (Buffer.byteLength(replacedText, 'utf8') > cap) {
|
|
145
|
+
ctx.logger.warn(`spill-policy: spill notice for ${toolName} exceeds maxInlineBytes; keeping the inline content`);
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return replacedText;
|
|
149
|
+
}
|
|
150
|
+
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
151
|
+
// Delegate first so a downstream listener (e.g. a hook) settles the result;
|
|
152
|
+
// we bound whatever it accepted. A block passes through — spill only shapes
|
|
153
|
+
// accepted plain-text results, never corrective feedback.
|
|
154
|
+
const decision = await next();
|
|
155
|
+
// Skip `read` to avoid a read → spill → read again loop.
|
|
156
|
+
if (decision.kind !== 'accept' || Object.hasOwn(decision, 'value')
|
|
157
|
+
|| exec.parent !== undefined || exec.name === 'read')
|
|
158
|
+
return decision;
|
|
159
|
+
const content = decision.content ?? result.content;
|
|
160
|
+
const text = flattenPlainText(content);
|
|
161
|
+
if (text === undefined)
|
|
162
|
+
return decision;
|
|
163
|
+
const totalBytes = Buffer.byteLength(text, 'utf8');
|
|
164
|
+
if (totalBytes <= maxInlineBytes)
|
|
165
|
+
return decision;
|
|
166
|
+
const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(exec), exec.name, exec.callId, 'result');
|
|
167
|
+
if (replacedText === undefined)
|
|
168
|
+
return decision;
|
|
169
|
+
const replaced = [{ type: 'text', text: replacedText }];
|
|
170
|
+
return { kind: 'accept', content: replaced, ...decision.additionalContexts ? { additionalContexts: decision.additionalContexts } : {} };
|
|
171
|
+
}, { prepend: true });
|
|
172
|
+
// The durable-log arm: bound the `tool/code-dispatch` event's copy of an
|
|
173
|
+
// oversized sub-call result the same way the model-facing arm bounds an
|
|
174
|
+
// outer result. The program's returned value is untouched (it already
|
|
175
|
+
// crossed the worker boundary whole); only the session log's copy shrinks
|
|
176
|
+
// to preview + locator, so replay and UIs read the full text through the
|
|
177
|
+
// spill artifact exactly as they do for spilled native results.
|
|
178
|
+
ctx.on('tools/ptc-dispatch-log', async (dispatch, next) => {
|
|
179
|
+
const content = await next();
|
|
180
|
+
// `read` sub-calls spill too: the log copy is not model context, so the
|
|
181
|
+
// read → spill → read-again loop the post-execute arm avoids cannot
|
|
182
|
+
// happen here, and read is precisely the tool that produces huge logs.
|
|
183
|
+
const text = flattenPlainText(content);
|
|
184
|
+
if (text === undefined)
|
|
185
|
+
return content;
|
|
186
|
+
const totalBytes = Buffer.byteLength(text, 'utf8');
|
|
187
|
+
if (totalBytes <= maxInlineBytes)
|
|
188
|
+
return content;
|
|
189
|
+
const replacedText = await spillReplacement(text, totalBytes, ownerSessionId(dispatch.exec), dispatch.name, dispatch.subCallId, 'dispatch');
|
|
190
|
+
if (replacedText === undefined)
|
|
191
|
+
return content;
|
|
192
|
+
return [{ type: 'text', text: replacedText }];
|
|
193
|
+
}, { prepend: true });
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Browser-safe formatting and recognition of persisted spill-policy notices. */
|
|
2
|
+
import { type Omitted } from '@deepseek-ai/dsh-output-retention';
|
|
3
|
+
import type { SpillRef } from '@deepseek-ai/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
|
+
* @returns the complete notice without a leading preview separator.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatSpillNotice(omitted: Omitted, ref: Pick<SpillRef, 'locator' | 'retrievalHint'>): string;
|
|
11
|
+
/**
|
|
12
|
+
* Recognize a final spill-policy notice in persisted text, including notice-only output.
|
|
13
|
+
* This identifies the text convention, not authenticated provenance of tool output.
|
|
14
|
+
* @param text - complete recorded text result.
|
|
15
|
+
* @returns whether a complete notice occupies the end of the result.
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasSpillNotice(text: string): boolean;
|
|
18
|
+
//# sourceMappingURL=notice.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Browser-safe formatting and recognition of persisted spill-policy notices. */
|
|
2
|
+
import { describeOmitted } from '@deepseek-ai/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
|
+
* @returns the complete notice without a leading preview separator.
|
|
16
|
+
*/
|
|
17
|
+
export function formatSpillNotice(omitted, ref) {
|
|
18
|
+
return `${OPEN}${describeOmitted(omitted, 'bytes')}${LOCATION}${ref.locator}${GUIDANCE_SEPARATOR}${ref.retrievalHint}${CLOSE}`;
|
|
19
|
+
}
|
|
20
|
+
function isOmission(text) {
|
|
21
|
+
if (text === describeOmitted({ kind: 'none' }, 'bytes')
|
|
22
|
+
|| text === describeOmitted({ kind: 'unknown' }, 'bytes'))
|
|
23
|
+
return true;
|
|
24
|
+
const count = Number(text.slice(COUNT_OFFSET, text.length - COUNT_SUFFIX.length));
|
|
25
|
+
return Number.isSafeInteger(count) && count >= 0
|
|
26
|
+
&& text === describeOmitted({ kind: 'exact', count }, 'bytes');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Recognize a final spill-policy notice in persisted text, including notice-only output.
|
|
30
|
+
* This identifies the text convention, not authenticated provenance of tool output.
|
|
31
|
+
* @param text - complete recorded text result.
|
|
32
|
+
* @returns whether a complete notice occupies the end of the result.
|
|
33
|
+
*/
|
|
34
|
+
export function hasSpillNotice(text) {
|
|
35
|
+
if (!text.endsWith(CLOSE))
|
|
36
|
+
return false;
|
|
37
|
+
let start = 0;
|
|
38
|
+
while (true) {
|
|
39
|
+
const next = text.indexOf(`${SEPARATOR}${OPEN}`, start);
|
|
40
|
+
const candidate = text.slice(start, next < 0 ? -CLOSE.length : next);
|
|
41
|
+
const location = candidate.indexOf(LOCATION, OPEN.length);
|
|
42
|
+
if (candidate.startsWith(OPEN) && location >= 0
|
|
43
|
+
&& isOmission(candidate.slice(OPEN.length, location))) {
|
|
44
|
+
return text.indexOf(GUIDANCE_SEPARATOR, start + location + LOCATION.length) >= 0;
|
|
45
|
+
}
|
|
46
|
+
if (next < 0)
|
|
47
|
+
return false;
|
|
48
|
+
start = next + SEPARATOR.length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=notice.js.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
|
+
* `@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
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-spill-policy",
|
|
3
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 API)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,33 +18,38 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
+
"./notice": {
|
|
22
|
+
"types": "./lib/types/notice.d.ts",
|
|
23
|
+
"default": "./lib/types/notice.js"
|
|
24
|
+
},
|
|
21
25
|
"./src/*": "./src/*",
|
|
22
26
|
"./package.json": "./package.json"
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"lib/index.js",
|
|
30
|
+
"lib/types/**/*.js",
|
|
26
31
|
"lib/types/**/*.d.ts"
|
|
27
32
|
],
|
|
28
33
|
"license": "MIT",
|
|
29
34
|
"peerDependencies": {
|
|
30
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
31
|
-
"@deepseek-ai/dsh-output-retention": "^0.1.
|
|
32
|
-
"@deepseek-ai/dsh-
|
|
33
|
-
"@deepseek-ai/dsh-
|
|
34
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
35
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-output-retention": "^0.1.3-alpha.2",
|
|
37
|
+
"@deepseek-ai/dsh-spill": "^0.1.3-alpha.2",
|
|
38
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
|
|
39
|
+
"@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
|
|
35
40
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
36
41
|
},
|
|
37
42
|
"dependencies": {
|
|
38
43
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
39
44
|
},
|
|
40
45
|
"devDependencies": {
|
|
41
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
47
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
46
|
+
"@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-code-runtime-worker-thread": "^0.1.3-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-spill": "^0.1.3-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-output-retention": "^0.1.3-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
|
|
48
53
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
49
54
|
}
|
|
50
55
|
}
|