diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index da386eec26..091bc50e64 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: 45fe3d7549b98a1c199e3bba1565fbf2c11b3620 -persistence-catalog.zh.md: f55524fdf2f0c4dc3167a89249be8e703bfca7c3 +persistence-catalog.md: d8d681934326b55ae743e71eb315582af2190104 +persistence-catalog.zh.md: 59c56b8e949d245c5ae95d59bf239f3a8069c932 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 45fe3d7549..d8d6819343 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -4929,7 +4929,7 @@ Sources: [`packages/llm/llm/src/types.ts:127`](../packages/llm/llm/src/types.ts) SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -Sources: [`packages/core/tools/src/index.ts:689`](../packages/core/tools/src/index.ts) +Sources: [`packages/core/tools/src/index.ts:699`](../packages/core/tools/src/index.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index f55524fdf2..59c56b8e94 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -4931,7 +4931,7 @@ SHA-256: `43e311235aa537df1bacdd92d144df8a952337a76e9ac892cdf670f1a797bad8` SHA-256: `3cfc3a56502da1f8c6153c2c56657bab4b749056968634dd0991d325ec1c919f` -来源:[`packages/core/tools/src/index.ts:689`](../packages/core/tools/src/index.ts) +来源:[`packages/core/tools/src/index.ts:699`](../packages/core/tools/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index e525fb8391..566b8c85f8 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -36275,7 +36275,7 @@ "packages/core/tools/src/index.ts#ToolRestriction" ], "sources": [ - "packages/core/tools/src/index.ts:689" + "packages/core/tools/src/index.ts:699" ] }, { diff --git a/docs/tool-execution-pipeline.i18n.yaml b/docs/tool-execution-pipeline.i18n.yaml index bcf6b13247..3d96cab19d 100644 --- a/docs/tool-execution-pipeline.i18n.yaml +++ b/docs/tool-execution-pipeline.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/tool-execution-pipeline.md -tool-execution-pipeline.md: c0abeb8e8de3710cd718b1136af480395ca64101 -tool-execution-pipeline.zh.md: 01816ca1b68b95e4789dffa00fa3c3c0d2881cdc +tool-execution-pipeline.md: 5e9b2079e1a5dde2a0423fdf6d7743e0ccbcc1e0 +tool-execution-pipeline.zh.md: 34bb84da5d5ecf2883c25220360cc50d2a1a105a diff --git a/docs/tool-execution-pipeline.md b/docs/tool-execution-pipeline.md index c0abeb8e8d..5e9b2079e1 100644 --- a/docs/tool-execution-pipeline.md +++ b/docs/tool-execution-pipeline.md @@ -3,7 +3,7 @@ # Tool Execution Pipeline -This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `finalizeContent` and `tools/result` run afterward. +This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward. ```mermaid flowchart TD diff --git a/docs/tool-execution-pipeline.zh.md b/docs/tool-execution-pipeline.zh.md index 01816ca1b6..34bb84da5d 100644 --- a/docs/tool-execution-pipeline.zh.md +++ b/docs/tool-execution-pipeline.zh.md @@ -5,7 +5,7 @@ [English](tool-execution-pipeline.md) | 中文 -此图展示策略、钩子、沙箱、文件系统守卫、结果重写、最终结果观察和 UI 渲染在不改变循环的情况下何时运行。`tools/pre-execute` waterfall(瀑布式事件)首先运行,随后是单调守卫,然后运行 `tools/execute` 和 `tools/post-execute` waterfall;这三个 waterfall 可以改写一次调用。由定义自身控制的 `finalizeContent` 和 `tools/result` 在此之后运行。 +此图展示策略、钩子、沙箱、文件系统守卫、结果重写、最终结果观察和 UI 渲染在不改变循环的情况下何时运行。`tools/pre-execute` waterfall(瀑布式事件)首先运行,随后是单调守卫,然后运行 `tools/execute` 和 `tools/post-execute` waterfall;这三个 waterfall 可以改写一次调用。由定义自身控制的 `projectContent` 在执行后策略之前安装已准备内容;`finalizeContent` 和 `tools/result` 在此之后运行。 ```mermaid flowchart TD diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 7d2f58ff7a..5a9b752e58 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -7068,7 +7068,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ToolDefinition', - declaration: 'export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n}', + declaration: 'export interface ToolDefinition extends ToolSchema {\n readonly output: ToolOutputDefinition;\n execute(args: unknown, exec: ToolRunContext): Promise;\n projectContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n finalizeContent?(exec: Readonly, result: Readonly): ContentBlock[] | undefined;\n timeoutMs?: number;\n isConcurrencySafe?(args: unknown): boolean;\n presentCall?(args: unknown): ToolCallView | undefined;\n presentResult?(args: unknown, result: ToolResult): ToolResultView | undefined;\n}', }, { name: 'ToolDispatchExecution', diff --git a/packages/llm/token-meter/README.i18n.yaml b/packages/llm/token-meter/README.i18n.yaml index d04470aeaa..d0008ecc2f 100644 --- a/packages/llm/token-meter/README.i18n.yaml +++ b/packages/llm/token-meter/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/token-meter/README.md -README.md: 268f75f1acda25440e08207e4ab13033ec397f5f -README.zh.md: fca37db43282b54a42666583824f215d5de8e930 +README.md: e6d957bd14560d967fb221b147d87d7db42197ea +README.zh.md: 526be7b2e399bcf6f4638ece9a7315044fa48c55 diff --git a/packages/llm/token-meter/README.md b/packages/llm/token-meter/README.md index 268f75f1ac..e6d957bd14 100644 --- a/packages/llm/token-meter/README.md +++ b/packages/llm/token-meter/README.md @@ -11,8 +11,6 @@ English | [中文](README.zh.md) Use `ctx.tokenMeter` to estimate a session's current request and context pressure or price one message. Measurements replay the durable session log, remain deterministic, and make no model calls, so compaction, occupancy displays, and telemetry can share one result. When session projections are available, consumers can read `tokenUsage`, `contextPressure`, and `contextBreakdown`; text and routes without image pricing use an approximate fixed heuristic, declared visual-token pricing applies when available, and files are priced as model-visible handle text. Provider-reported usage is reused only for an identical request envelope; the package adds no model-visible content and makes no loop decisions. -`./estimate` exports the stateless text/content estimators used by tool-result retention. Model adapters supply request-image costs through `imageRequestPricing`. - ## Table of Contents - [Use this package](#use-this-package) @@ -104,6 +102,8 @@ Each `measure()` call synchronizes the fold to the current durable tail, then re `contextBreakdown` retains plain-JSON `{ seq, heuristicTokens, system }` entries in surface order and reuses the measurement plan/commit fold. Its state and surface transitions are O(current retained surface), not O(1) and not O(total historical log); replaced entries and message bodies are not retained. State version 5 invalidates checkpoints priced with offload metadata. `contextPressure` remains the scalar shadow-price consumer: replacements without adjacent claims contribute zero delta. The usage fold retains one last-sample slot because legal logs never report usage for an earlier step after a later step reports usage. +`./estimate` exports the stateless text/content estimators used by tool-result retention. Model adapters supply request-image costs through `imageRequestPricing`. + ----- diff --git a/packages/llm/token-meter/README.zh.md b/packages/llm/token-meter/README.zh.md index fca37db432..526be7b2e3 100644 --- a/packages/llm/token-meter/README.zh.md +++ b/packages/llm/token-meter/README.zh.md @@ -11,8 +11,6 @@ kind: "package-reference" 使用 `ctx.tokenMeter` 估算会话当前的请求与上下文压力,或为单条消息计价。测量会回放持久会话日志,结果确定且不进行模型调用,因此压缩、占用显示与遥测可以共享同一结果。会话投影可用时,消费方可以读取 `tokenUsage`、`contextPressure` 与 `contextBreakdown`;文本和没有图片定价的路由采用近似的固定启发式规则,存在声明时应用视觉 token 定价,文件则按模型可见的句柄文本计价。只有请求 envelope 完全相同时才复用提供方报告的用量;本包不添加模型可见内容,也不在 loop 中做决策。 -`./estimate` 导出无服务状态的文字与内容估算函数,供工具结果保留复用。图片的实际请求成本由模型适配器的 `imageRequestPricing` 提供。 - ## 目录 - [使用本包](#use-this-package) @@ -104,6 +102,8 @@ const price = ctx.tokenMeter.estimateMessage(message) `contextBreakdown` 按 surface 顺序保留纯 JSON 的 `{ seq, heuristicTokens, system }` 条目,并复用测量服务的 plan/commit fold。其状态与 surface 转换成本为 O(当前保留 surface),不是 O(1),也不是 O(完整历史日志);被替换条目和消息正文不保留。状态版本 5 使计入省略元数据的检查点失效。`contextPressure` 仍是标量影子价消费方:没有相邻 claim 的替换贡献零增量。用量 fold 保留一个最后样本槽,因为合法日志不会在更晚步骤报告用量后再次报告更早步骤的用量。 +`./estimate` 导出无服务状态的文字与内容估算函数,供工具结果保留复用。图片的实际请求成本由模型适配器的 `imageRequestPricing` 提供。 + ----- diff --git a/packages/mcp/mcp-client/tests/mcp-client.spec.ts b/packages/mcp/mcp-client/tests/mcp-client.spec.ts index 7e136b4169..47747178b4 100644 --- a/packages/mcp/mcp-client/tests/mcp-client.spec.ts +++ b/packages/mcp/mcp-client/tests/mcp-client.spec.ts @@ -698,7 +698,7 @@ describe('tool execution', () => { }) expect(result.isError).toBe(false) expect(result.content.map(block => block.type)).toEqual(['text', 'image']) - expect(textAt(result.content)).toBe(blocks[0].text?.slice(0, 20)) + expect(textAt(result.content)).toBe('long text'.repeat(1000).slice(0, 20)) if (result.isError) throw new Error('expected MCP success') expect(result.value).toEqual({ content: blocks }) } finally { diff --git a/packages/web/tool-web/package.json b/packages/web/tool-web/package.json index b41d822dca..f12ac855bb 100644 --- a/packages/web/tool-web/package.json +++ b/packages/web/tool-web/package.json @@ -53,6 +53,7 @@ "@deepseek-ai/dsh-web-fetch-http": "workspace:^", "@deepseek-ai/dsh-web-search-exa": "workspace:^", "@types/turndown": "^5.0.6", - "@deepseek-ai/dsh-scope": "workspace:^" + "@deepseek-ai/dsh-scope": "workspace:^", + "@deepseek-ai/dsh-token-meter": "workspace:^" } } diff --git a/packages/web/tool-web/tests/spill.spec.ts b/packages/web/tool-web/tests/spill.spec.ts index c91e5232ea..6f5a858462 100644 --- a/packages/web/tool-web/tests/spill.spec.ts +++ b/packages/web/tool-web/tests/spill.spec.ts @@ -14,6 +14,7 @@ import { mkdtempSync, readFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' +import { estimateContent } from '@deepseek-ai/dsh-token-meter/estimate' import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -37,7 +38,7 @@ let spillRoot: string let ctx: Context const BODY = 'X'.repeat(4000) // formatted result is well over the policy cap -const MAX_INLINE_BYTES = 1000 // leaves room for a head/tail preview beside the notice +const MAX_INLINE_TOKENS = 250 // leaves room for a head/tail preview beside the notice beforeEach(async () => { vi.spyOn(publicHttpNetwork, 'resolve').mockResolvedValue([{ address: '127.0.0.1', family: 4 }]) @@ -55,12 +56,13 @@ beforeEach(async () => { // policy cap is what triggers the spill (the Agent Note's separation of concerns). await ctx.plugin(WebFetchLocal, { maxBodyChars: 500_000 }) await ctx.plugin(LocalSpillStore, { root: spillRoot }) - await ctx.plugin(SpillPolicy, { maxInlineTokens: MAX_INLINE_BYTES }) + await ctx.plugin(SpillPolicy, { maxInlineTokens: MAX_INLINE_TOKENS }) await ctx.plugin(ToolWeb) }) afterEach(async () => { vi.restoreAllMocks() + await ctx.fiber.dispose() await new Promise(resolve => server.close(() => { resolve() })) rmSync(spillRoot, { recursive: true, force: true }) }) @@ -80,7 +82,7 @@ describe('web_fetch spill showcase', () => { // Model-facing text is a preview + notice within the cap, NOT the full body. expect(text.length).toBeLessThan(BODY.length) - expect(Buffer.byteLength(text, 'utf8')).toBeLessThanOrEqual(MAX_INLINE_BYTES) + expect(estimateContent([{ type: 'text', text }])).toBeLessThanOrEqual(MAX_INLINE_TOKENS) expect(text).toContain(`Fetched ${base}`) // the head of the formatted result survives expect(text).toContain('Full formatted result stored at:') expect(text).toContain('Use read with offset/limit, or grep this path') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 550fc6f940..1afa3527f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12352,6 +12352,9 @@ importers: '@deepseek-ai/dsh-system-prompt': specifier: workspace:^ version: link:../../core/system-prompt + '@deepseek-ai/dsh-token-meter': + specifier: workspace:^ + version: link:../../llm/token-meter '@deepseek-ai/dsh-tool-call-timeout-policy': specifier: workspace:^ version: link:../../guard/timeout-policy diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 1c676c29d7..4520fa66f6 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -1511,7 +1511,7 @@ function renderToolPipeline(): string { const maintenance = 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs' return [ ...generatedHeader('Tool Execution Pipeline'), - 'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `finalizeContent` and `tools/result` run afterward.', + 'This graph shows where policy, hooks, sandboxing, filesystem guards, result rewriting, final-outcome observation, and UI rendering run without changing the loop. The `tools/pre-execute` waterfall runs first, monotonic guards run next, and the `tools/execute` and `tools/post-execute` waterfalls follow; the three waterfalls may transform a call. Definition-owned `projectContent` installs prepared content before post-execute; `finalizeContent` and `tools/result` run afterward.', '', '```mermaid', 'flowchart TD', @@ -1526,6 +1526,7 @@ function renderToolPipeline(): string { ' toolBody["Registered tool execute() body"]', ` fsGate["${mermaidCode('fs/write-intent')} or ${mermaidCode('fs/edit-intent')}
tool-fs mutations only"]`, ` owned["Tool-owned session events
${mermaidCode('todo/write')}, ${mermaidCode('fs/observed')}, ${mermaidCode('hook/invoked')}, ${mermaidCode('hook/result')}, ${mermaidCode('tool/ptc-dispatch')}"]`, + ' project["ToolDefinition.projectContent
execution-prepared text and images"]', ` post["${mermaidCode('tools/post-execute')} waterfall
accept, block, replace, add context"]`, ' normalized["Registry outer normalization
pipeline/result snapshot throws become isError"]', ' finalize["ToolDefinition.finalizeContent
last content-only invariant"]', @@ -1553,7 +1554,9 @@ function renderToolPipeline(): string { ' fsGate --> toolBody', ' toolBody --> owned', ' toolBody --> around', - ' around --> post', + ' around --> project', + ' project --> post', + ' project -.->|throw| normalized', ' around -.->|wrapper throws| normalized', ' post -.->|throw| normalized', ' post --> finalize',