diff --git a/packages/session/session-format-v3-to-v4/README.i18n.yaml b/packages/session/session-format-v3-to-v4/README.i18n.yaml index 5f7a913ffa..7259494569 100644 --- a/packages/session/session-format-v3-to-v4/README.i18n.yaml +++ b/packages/session/session-format-v3-to-v4/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/session/session-format-v3-to-v4/README.md -README.md: 352246595a3da2d5902c51ebf946595c30b0247e -README.zh.md: e2eebbd642122d4e0d71d379f8f342dd8d8b37f4 +README.md: 0cbdc50675085733fc5a8028e5456e29ec65f695 +README.zh.md: 29764dd973a61b6ee43e5c98a3908df29b3b803c diff --git a/packages/session/session-format-v3-to-v4/README.md b/packages/session/session-format-v3-to-v4/README.md index 352246595a..0cbdc50675 100644 --- a/packages/session/session-format-v3-to-v4/README.md +++ b/packages/session/session-format-v3-to-v4/README.md @@ -100,7 +100,7 @@ A malformed canonical wrapper raises a format error; an unmapped wrapper extensi ### Extension data -Unknown V3 content tags become `plugin:`; all other fields remain unchanged and opaque. Stream starts use the same tag in `blockType` and retain `type` and `index`; other start-owned field names receive a `plugin:` prefix. Request-tool definitions retain `name`, `description`, and `parameters`; all other own field names receive `plugin:` too, so historical `deferLoading` becomes `plugin:deferLoading` without enabling V4 capabilities. Existing prefixes double, keeping distinct old names distinct. Parameters, arguments, replay state, and plugin content fields are not traversed. These names preserve data identity; they do not load or execute plugins. +Unknown V3 content tags become `plugin:`; all other fields remain unchanged and opaque. Stream starts use the same tag in `blockType`; every other field retains its original key and value. Request-tool definitions retain `name`, `description`, and `parameters`; all other own field names receive `plugin:` too, so historical `deferLoading` becomes `plugin:deferLoading` without enabling V4 capabilities. Existing prefixes double, keeping distinct old names distinct. Parameters, arguments, replay state, and plugin content fields are not traversed. These names preserve data identity; they do not load or execute plugins. ### Message-source conversion diff --git a/packages/session/session-format-v3-to-v4/README.zh.md b/packages/session/session-format-v3-to-v4/README.zh.md index e2eebbd642..29764dd973 100644 --- a/packages/session/session-format-v3-to-v4/README.zh.md +++ b/packages/session/session-format-v3-to-v4/README.zh.md @@ -100,7 +100,7 @@ Wrapper 只允许 `type`、`toolCallId`、`content` 和 `isError`。未知 wrapp ### 扩展数据 -V3 未知内容标签变为 `plugin:`,其他字段原样保留且不作解释。流起始块的 `blockType` 使用相同标签,并保留 `type` 和 `index`,其他自有字段名称添加 `plugin:` 前缀。请求工具定义保留 `name`、`description` 和 `parameters`,其他自有字段名称同样添加 `plugin:`;历史 `deferLoading` 因此变为 `plugin:deferLoading`,不会启用 V4 能力。已有前缀会再次添加,使不同旧名称保持不同。不遍历参数、实参、回放状态和插件内容字段。这些名称保留数据身份,不会加载或执行插件。 +V3 未知内容标签变为 `plugin:`,其他字段原样保留且不作解释。流起始块的 `blockType` 使用相同标签,其他字段的原始键和值均保留。请求工具定义保留 `name`、`description` 和 `parameters`,其他自有字段名称同样添加 `plugin:`;历史 `deferLoading` 因此变为 `plugin:deferLoading`,不会启用 V4 能力。已有前缀会再次添加,使不同旧名称保持不同。不遍历参数、实参、回放状态和插件内容字段。这些名称保留数据身份,不会加载或执行插件。 ### 消息来源转换 diff --git a/packages/session/session-format-v3-to-v4/src/content.ts b/packages/session/session-format-v3-to-v4/src/content.ts index 67062e9c17..0dd45b228f 100644 --- a/packages/session/session-format-v3-to-v4/src/content.ts +++ b/packages/session/session-format-v3-to-v4/src/content.ts @@ -5,7 +5,6 @@ import type { SessionFormatEvent, SessionFormatJsonObject, SessionFormatJsonValu import { mapEventMessages } from './sources.ts' const V3_BLOCK_TYPES = new Set(['text', 'reasoning', 'image', 'file', 'tool-call', 'tool-result']) -const START_FIELDS = new Set(['type', 'index', 'blockType']) const TOOL_SCHEMA_FIELDS = new Set(['name', 'description', 'parameters']) /** Prefix extension-owned field names without reinterpreting their values. */ @@ -51,8 +50,7 @@ function migrateChunk(value: SessionFormatJsonValue | undefined, subject: string const original = value['blockType'] if (typeof original !== 'string') throw new SessionFormatError(`${subject} blockType must be a string`) const blockType = V3_BLOCK_TYPES.has(original) ? original : `plugin:${original}` - const mapped = namespaceFields(value, START_FIELDS) - return blockType === original ? mapped : { ...mapped, blockType } + return blockType === original ? value : { ...value, blockType } } /** diff --git a/packages/session/session-format-v3-to-v4/tests/content.spec.ts b/packages/session/session-format-v3-to-v4/tests/content.spec.ts index 962de79b40..12c2f30804 100644 --- a/packages/session/session-format-v3-to-v4/tests/content.spec.ts +++ b/packages/session/session-format-v3-to-v4/tests/content.spec.ts @@ -71,7 +71,7 @@ describe('V3 content conversion', () => { }) it.each(['assistant/message', 'assistant/attempt'])('converts raw %s start/end blocks and preserves start-owned fields', (type) => { - const chunk = (value: SessionFormatJsonValue): SessionFormatJsonObject => ({ type: 'chunk', chunk: value, elapsedMs: 7 }) + const chunk = (value: SessionFormatJsonValue): SessionFormatJsonObject => ({ type: 'chunk', chunk: value, time: 7 }) const stream = [ chunk({ type: 'block-start', index: 0, blockType: 'future', extensionName: 'old', metadata: { old: true }, custom: wrapper }), chunk({ type: 'block-end', index: 0, block: wrapper }), @@ -85,11 +85,11 @@ describe('V3 content conversion', () => { ] const actual = migrateV3EventContent(event(type, { message: { ...user, content: [] }, stream })) expect(actual.data).toEqual({ message: { ...user, content: [] }, stream: [ - chunk({ type: 'block-start', index: 0, blockType: 'plugin:future', 'plugin:extensionName': 'old', 'plugin:metadata': { old: true }, 'plugin:custom': wrapper }), + chunk({ type: 'block-start', index: 0, blockType: 'plugin:future', extensionName: 'old', metadata: { old: true }, custom: wrapper }), chunk({ type: 'block-end', index: 0, block: convertedBlock }), chunk({ type: 'block-start', index: 1, blockType: 'plugin:message' }), chunk({ type: 'block-end', index: 1, block: { type: 'plugin:message', content: [wrapper] } }), - stream[4], chunk({ type: 'block-start', index: 3, blockType: 'text', 'plugin:metadata': { original: true } }), + stream[4], stream[5], ...stream.slice(6), ] }) const unchanged = event(type, { message: { ...user, content: [] }, stream: stream.slice(6) }) @@ -114,7 +114,7 @@ describe('V3 content conversion', () => { expect(tool['deferLoading']).toBe(deferLoading) }) - it('keeps original prefixed keys distinct from unprefixed tool and stream extras', () => { + it('prefixes tool extension keys while preserving stream-start extra keys', () => { const extra = JSON.parse('{"deferLoading":true,"plugin:deferLoading":false,"metadata":{"saved":1},"plugin:metadata":{"saved":2},"__proto__":{"saved":3},"plugin:__proto__":{"saved":4}}') as SessionFormatJsonObject const expected = { 'plugin:deferLoading': true, 'plugin:plugin:deferLoading': false, @@ -126,7 +126,7 @@ describe('V3 content conversion', () => { expect(request.data).toEqual({ header: { tools: [{ name: 'echo', description: 'Echo', parameters: tool.parameters, ...expected }] } }) const start = { type: 'block-start', index: 7, blockType: 'plugin:text', ...extra } const stream = migrateV3EventContent(event('assistant/attempt', { stream: [{ type: 'chunk', chunk: start }] })) - expect(stream.data).toEqual({ stream: [{ type: 'chunk', chunk: { type: 'block-start', index: 7, blockType: 'plugin:plugin:text', ...expected } }] }) + expect(stream.data).toEqual({ stream: [{ type: 'chunk', chunk: { ...start, blockType: 'plugin:plugin:text' } }] }) expect(extra['deferLoading']).toBe(true) expect(Object.hasOwn(extra, '__proto__')).toBe(true) })