diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml index 47dbb4329a..74dbfe69df 100644 --- a/packages/experimental/agent-team/README.i18n.yaml +++ b/packages/experimental/agent-team/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/experimental/agent-team/README.md -README.md: 6e2b55018971ec96802de0e5be042d49b45c674a -README.zh.md: b497d3790e43a9bc843e2a6b88c9755b698034b5 +README.md: 31ea956d2e39dfe25410929be2a574b4c3f07a7d +README.zh.md: 03344285ceaee965c4154f939f22b592c833840b diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index 6e2b550189..31ea956d2e 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -145,6 +145,8 @@ Tasks are complete versioned snapshots; every mutation carries `expectedRevision Team events are appended to the exact live Lead Session and flushed before the operation reports success or wakes waiters. `team/member`, `team/task`, `team/message/queued`, and `team/message/delivered` are log-only: they never enter the conversation surface, so derived model history is untouched by coordination records. Session event `seq` and `time` own ordering and timing; snapshots do not duplicate them. The `./invariant` companion replays each candidate Team event against its committed prefix and rejects invalid transitions before append. +Native V4 Team event and checkpoint admission reject retired `tool-result` content before it can enter mailbox state. Historical conversion belongs to the Session-format migration; the Team projection does not convert old wrappers. + ### Disposal Disposal closes admission, aborts and awaits admitted creation and mailbox-dispatch transactions, then asks the continuation owner to release the roster's exact live direct children and their descendants; non-Team continuable children of the Lead remain untouched. Cleanup failures make disposal fail visibly, bounded by `disposalTimeoutMs`. diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index b497d3790e..03344285ce 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -145,6 +145,8 @@ Lead 可以停止 teammate 的当前轮次,而不会删除其排队的消息 Team 事件追加到精确的 live Lead 会话,并在操作报告成功或唤醒等待者之前 flush。`team/member`、`team/task`、`team/message/queued` 与 `team/message/delivered` 仅存在于日志:它们从不进入会话表面,因此派生模型历史不受协作记录影响。顺序与时间由会话事件的 `seq` 与 `time` 负责,快照不重复保存。`./invariant` 伴生插件把每条候选 Team 事件对照已提交前缀回放,并在 append 前拒绝非法转换。 +原生 V4 的 Team 事件及检查点准入会拒绝退役的 `tool-result` 内容,防止它进入邮箱状态。历史转换由 Session 格式迁移负责,Team 投影不转换旧包装。 + ### Dispose dispose 会关闭准入、中止并等待已获准的创建与 mailbox dispatch 事务,再让 continuation owner 释放 roster 中确切的 live direct child 及其后代;Lead 的非 Team continuable child 不受影响。cleanup 失败会让 dispose 明确失败,并以 `disposalTimeoutMs` 为上限。 diff --git a/packages/experimental/agent-team/src/projection.ts b/packages/experimental/agent-team/src/projection.ts index 85fdfd0e0d..d5dff9784c 100644 --- a/packages/experimental/agent-team/src/projection.ts +++ b/packages/experimental/agent-team/src/projection.ts @@ -40,8 +40,8 @@ const imageAttachmentSchema = z.object({ name: z.string().optional(), }).strict() -// ContentBlockMap is merge-extensible. Validate every core variant exactly, -// while retaining JSON-decoded plugin variants under an unknown type tag. +// Validate the listed variants; retired tool-result tags cannot enter the +// merge-extensible fallback for JSON-decoded plugin content. const contentBlockSchema: z.ZodType = z.lazy(() => z.union([ z.object({ type: z.literal('text'), text: z.string() }).strict(), z.object({ type: z.literal('reasoning'), text: z.string() }).strict(), @@ -52,12 +52,6 @@ const contentBlockSchema: z.ZodType = z.lazy(() => z.union([ name: z.string(), arguments: z.string(), }).strict(), - z.object({ - type: z.literal('tool-result'), - toolCallId: z.string().min(1), - content: z.array(contentBlockSchema), - isError: z.boolean().optional(), - }).strict(), z.object({ type: z.string().min(1) }).loose().refine( block => !coreContentBlockTypes.has(block.type), { message: 'known content block types must match their declared fields' }, diff --git a/packages/experimental/agent-team/tests/projection-events.spec.ts b/packages/experimental/agent-team/tests/projection-events.spec.ts index 5f9728a979..e5443d4135 100644 --- a/packages/experimental/agent-team/tests/projection-events.spec.ts +++ b/packages/experimental/agent-team/tests/projection-events.spec.ts @@ -1,6 +1,8 @@ -import { describe, expect, it } from 'vitest' -import { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' +import { describe, expect, it, onTestFinished } from 'vitest' +import { Context } from '@deepseek-ai/cordis' +import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionEventMap, SessionEventType } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { teamProjectionDefinition } from '../src/projection.ts' import type { TeamProjectionState, TeamState } from '../src/projection.ts' import { TeamId, TeamMessageId, TeamTaskId } from '../src/types.ts' @@ -82,6 +84,28 @@ function message(overrides: Partial = {}): TeamMessageSnaps } describe('Agent Teams projection events', () => { + it('rejects retired tool-result content when restoring a native V4 Team checkpoint', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + ctx.effect(() => ctx.sessionProjections.register(teamProjectionDefinition)) + const session = ctx.sessions.create(ROOT) + const restore = (val: unknown) => ctx.sessionProjections.restore({ + agentTeam: { ver: teamProjectionDefinition.stateVersion, seq: -1, val }, + }, [], SessionLogOffset(0), session.header, SessionLogOffset(0)) + const valid = { ...project(ROOT, []), messages: [message()] } + expect(restore(JSON.parse(JSON.stringify(valid))).checkpoint['agentTeam']?.val).toEqual(valid) + const retiredMessage = message({ content: [{ + type: 'tool-result', toolCallId: 'retired', content: [{ type: 'text', text: 'old result' }], + }] as unknown as TeamMessageSnapshot['content'] }) + const retired = { ...valid, messages: [retiredMessage] } + expect(() => restore(JSON.parse(JSON.stringify(retired)))).toThrow() + expect(() => projectTeam(ROOT, [event('team/message/queued', { + version: 2, teamId: TEAM, message: retiredMessage, + }, SessionSeq(0))])).toThrow(/team\/message\/queued payload is invalid/) + }) + it('projects current-team records independently from inherited records', () => { const records: SessionEvent[] = [ event('team/member', { version: 2, teamId: TeamId('ancestor'), member: member() }, SessionSeq(0)),