diff --git a/README.md b/README.md index d2c1485..cdcceb0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ license CI node - tests + tests coverage Awesome

@@ -183,7 +183,7 @@ dsh web ```bash cd dsh-mneme && npm install -npm test # 1479 个测试 +npm test # 1481 个测试 npm run stress # 三轴线压测 npm run sync # src → lib 同步 ``` @@ -366,7 +366,7 @@ The plugin ships a zero-dependency stdio MCP server (standalone npm package **`m ```bash cd dsh-mneme && npm install -npm test # 1479 tests +npm test # 1481 tests npm run stress # three-axis stress test npm run sync # src → lib sync ``` diff --git a/dsh-mneme/README.md b/dsh-mneme/README.md index 5bd0854..bc09924 100644 --- a/dsh-mneme/README.md +++ b/dsh-mneme/README.md @@ -5,7 +5,7 @@ [![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme) [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) -[![tests](https://img.shields.io/badge/tests-1479%20passed-success)](https://github.com/slow-stack/mneme) +[![tests](https://img.shields.io/badge/tests-1481%20passed-success)](https://github.com/slow-stack/mneme) [![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions) [![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org) [![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme) @@ -584,7 +584,7 @@ src/ ├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列) └── index.js # 插件接线 lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖) -test/ # 1479 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) +test/ # 1481 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验) scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单 ``` @@ -593,7 +593,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压 ```bash cd dsh-mneme npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试) -npm test # 运行 1479 个测试 +npm test # 运行 1481 个测试 npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM) npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行) ``` diff --git a/dsh-mneme/lib/summarize.js b/dsh-mneme/lib/summarize.js index bfe2d65..442bc20 100644 --- a/dsh-mneme/lib/summarize.js +++ b/dsh-mneme/lib/summarize.js @@ -286,9 +286,14 @@ function collectMessages(session, maxChars = 8000, language = "zh", afterSeq, th break; } case "tool/result": { - const result = Array.isArray(data?.message?.content) - ? data.message.content.find((block) => block?.type === "tool-result") - : undefined; + // 工具结果有两种形状:旧形状把载荷与 isError 装在 content 内的 + // `type:"tool-result"` 块上;DSH 0.2.x 起内核改为直接投递 `role:"tool"` + // 消息本身,content 里只有 `type:"text"` 块、isError 挂在消息上。只认 + // 旧块形状时新形状落进 undefined 分支,工具输出与工具报错被静默丢弃。 + const message = data?.message; + const blocks = Array.isArray(message?.content) ? message.content : []; + const legacy = blocks.find((block) => block?.type === "tool-result"); + const result = legacy ?? (message?.role === "tool" ? message : undefined); const out = textOf(result?.content); const status = result?.isError === true ? STR.statusFail[language] : STR.statusOk[language]; lines.push(STR.transcriptToolResult[language](status, trim(out, 500))); diff --git a/dsh-mneme/src/summarize.js b/dsh-mneme/src/summarize.js index bfe2d65..442bc20 100644 --- a/dsh-mneme/src/summarize.js +++ b/dsh-mneme/src/summarize.js @@ -286,9 +286,14 @@ function collectMessages(session, maxChars = 8000, language = "zh", afterSeq, th break; } case "tool/result": { - const result = Array.isArray(data?.message?.content) - ? data.message.content.find((block) => block?.type === "tool-result") - : undefined; + // 工具结果有两种形状:旧形状把载荷与 isError 装在 content 内的 + // `type:"tool-result"` 块上;DSH 0.2.x 起内核改为直接投递 `role:"tool"` + // 消息本身,content 里只有 `type:"text"` 块、isError 挂在消息上。只认 + // 旧块形状时新形状落进 undefined 分支,工具输出与工具报错被静默丢弃。 + const message = data?.message; + const blocks = Array.isArray(message?.content) ? message.content : []; + const legacy = blocks.find((block) => block?.type === "tool-result"); + const result = legacy ?? (message?.role === "tool" ? message : undefined); const out = textOf(result?.content); const status = result?.isError === true ? STR.statusFail[language] : STR.statusOk[language]; lines.push(STR.transcriptToolResult[language](status, trim(out, 500))); diff --git a/dsh-mneme/test/summarize.test.js b/dsh-mneme/test/summarize.test.js index ad09a42..1e8b533 100644 --- a/dsh-mneme/test/summarize.test.js +++ b/dsh-mneme/test/summarize.test.js @@ -110,6 +110,25 @@ function toolResultEvent(text, isError, seq) { }; } +// DSH 0.2.x 内核对工具结果的实际投递形状:内核给的是 `role:"tool"` 消息本身, +// content 里只有 `type:"text"` 块、isError 挂在消息上,不再有 tool-result 块。 +// 蒸馏曾只认旧块形状,于是真实会话里的工具输出与工具报错全部被静默丢弃。 +function toolRoleResultEvent(text, isError, seq) { + return { + seq, + type: "tool/result", + data: { + message: { + role: "tool", + source: { kind: "tool", callId: `call-${seq}` }, + toolCallId: `call-${seq}`, + content: [{ type: "text", text }], + isError + } + } + }; +} + function codeDispatchEvent(text, isError, seq) { return { seq, @@ -1058,6 +1077,47 @@ test("collects real tool result and code dispatch payloads with failure status", assert.match(transcript, /代码执行(成功):代码执行结果/); }); +test("collects tool results delivered as role:tool messages, keeping the legacy block shape", async () => { + const { events, calls } = setup({ distillRateLimitIntervalMs: 0 }); + const handler = events.find((e) => e.name === "session/event").fn; + const session = { + id: "s-tool-role-shape", + requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }), + events: [ + userMessage("检查工具结果两种形状", 1), + toolResultEvent("旧形状载荷", false, 2), + toolRoleResultEvent("新形状载荷", false, 3), + toolRoleResultEvent("TypeError: tool crashed", true, 4), + { seq: 5, type: "turn/end" } + ] + }; + + await handler(session, { seq: 5, type: "turn/end" }); + const transcript = calls[0].messages.find((message) => message.role === "user").content[0].text; + assert.match(transcript, /工具结果(成功):旧形状载荷/); + assert.match(transcript, /工具结果(成功):新形状载荷/); + assert.match(transcript, /工具结果(失败):TypeError: tool crashed/); +}); + +test("keeps prefix trimming for tool results delivered as role:tool messages", async () => { + const { events, calls } = setup({ distillRateLimitIntervalMs: 0 }); + const handler = events.find((e) => e.name === "session/event").fn; + const session = { + id: "s-tool-role-trim", + requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }), + events: [ + userMessage("检查新形状截断", 1), + toolRoleResultEvent(`OUT_START ${"c".repeat(550)} OUT_END`, false, 2), + { seq: 3, type: "turn/end" } + ] + }; + + await handler(session, { seq: 3, type: "turn/end" }); + const transcript = JSON.stringify(calls[0].messages); + assert.ok(transcript.includes("OUT_START")); + assert.ok(!transcript.includes("OUT_END"), "role:tool 结果同样走 500 字符前缀截断"); +}); + test("collects subagent delivery once across inbox and user-message views", async () => { const { events, calls } = setup({ distillRateLimitIntervalMs: 0 }); const handler = events.find((e) => e.name === "session/event").fn;