




















@@ -33,6 +33,7 @@ function createMessageUpdateContext(
3333state?: Record<string, unknown>;
3434} = {},
3535) {
36+const partialReplyDirectiveAccumulator = createStreamingDirectiveAccumulator();
3637return {
3738params: {
3839runId: "run-1",
@@ -66,7 +67,11 @@ function createMessageUpdateContext(
6667log: { debug: params.debug ?? vi.fn() },
6768noteLastAssistant: vi.fn(),
6869stripBlockTags: params.stripBlockTags ?? vi.fn((text: string) => text),
69-consumePartialReplyDirectives: params.consumePartialReplyDirectives ?? vi.fn(() => null),
70+consumePartialReplyDirectives:
71+params.consumePartialReplyDirectives ??
72+vi.fn((text: string, options?: { final?: boolean }) =>
73+partialReplyDirectiveAccumulator.consume(text, options),
74+),
7075emitReasoningStream: vi.fn(),
7176flushBlockReplyBuffer: params.flushBlockReplyBuffer ?? vi.fn(),
7277resetAssistantMessageState: params.resetAssistantMessageState ?? vi.fn(),
@@ -332,6 +337,85 @@ describe("handleMessageUpdate text signatures", () => {
332337]);
333338});
334339340+it("holds incomplete streaming directive tails without emitting them as text", () => {
341+const onAgentEvent = vi.fn();
342+const accumulator = createStreamingDirectiveAccumulator();
343+const context = createMessageUpdateContext({
344+ onAgentEvent,
345+consumePartialReplyDirectives: vi.fn((text: string, options?: { final?: boolean }) =>
346+accumulator.consume(text, options),
347+),
348+});
349+350+const createNonPhaseEvent = (delta: string) =>
351+({
352+type: "message_update",
353+message: { role: "assistant", content: [] },
354+assistantMessageEvent: {
355+type: "text_delta",
356+ delta,
357+},
358+}) as never;
359+360+handleMessageUpdate(context, createNonPhaseEvent("Hello\n"));
361+handleMessageUpdate(context, createNonPhaseEvent("M"));
362+363+expect(onAgentEvent).toHaveBeenCalledTimes(1);
364+expect(firstMockArg(onAgentEvent, "agent event")).toMatchObject({
365+stream: "assistant",
366+data: { text: "Hello", delta: "Hello" },
367+});
368+expect(context.state.lastStreamedAssistantCleaned).toBe("Hello");
369+});
370+371+it("keeps stripped reply directives out of later plain deltas", () => {
372+const onAgentEvent = vi.fn();
373+const context = createMessageUpdateContext({ onAgentEvent });
374+375+const createNonPhaseEvent = (delta: string) =>
376+({
377+type: "message_update",
378+message: { role: "assistant", content: [] },
379+assistantMessageEvent: {
380+type: "text_delta",
381+ delta,
382+},
383+}) as never;
384+385+handleMessageUpdate(context, createNonPhaseEvent("[[reply_to_current]]\nHello"));
386+handleMessageUpdate(context, createNonPhaseEvent(" world"));
387+388+expect(onAgentEvent.mock.calls.map(([event]) => event)).toMatchObject([
389+{
390+stream: "assistant",
391+data: { text: "Hello", delta: "Hello" },
392+},
393+{
394+stream: "assistant",
395+data: { text: "Hello world", delta: " world" },
396+},
397+]);
398+});
399+400+it("does not expose complete legacy media directives on plain deltas", () => {
401+const onAgentEvent = vi.fn();
402+const context = createMessageUpdateContext({ onAgentEvent });
403+404+handleMessageUpdate(context, {
405+type: "message_update",
406+message: { role: "assistant", content: [] },
407+assistantMessageEvent: {
408+type: "text_delta",
409+delta: "Here it is.\nMEDIA:/tmp/final.png\n",
410+},
411+} as never);
412+413+expect(firstMockArg(onAgentEvent, "agent event")).toMatchObject({
414+stream: "assistant",
415+data: { text: "Here it is.", delta: "Here it is." },
416+});
417+});
418+335419it("uses full partial text for suffix deltas after a suppressed commentary item", () => {
336420const onAgentEvent = vi.fn();
337421const context = createMessageUpdateContext({ onAgentEvent });
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。