docs: document embedded subscribe text streaming

This commit is contained in:
Peter Steinberger
2026-06-04 15:00:33 -04:00
parent fc7f96c826
commit 60cb5d633f
5 changed files with 27 additions and 0 deletions
@@ -1,3 +1,5 @@
// Text-end repeat tests ensure repeated full snapshots do not duplicate block
// replies or assistantTexts.
import { describe, expect, it, vi } from "vitest";
import {
createTextEndBlockReplyHarness,
@@ -21,6 +23,8 @@ describe("subscribeEmbeddedAgentSession", () => {
expect(subscription.assistantTexts).toEqual(["Good morning!"]);
});
it("does not duplicate block chunks when text_end repeats full content", async () => {
// Chunked deltas may already flush all visible text before text_end repeats
// the same content; the snapshot must be ignored.
const onBlockReply = vi.fn();
const { emit } = createTextEndBlockReplyHarness({
onBlockReply,
@@ -1,3 +1,5 @@
// Duplicate block reply tests cover repeated text_end and message_end events
// from providers that replay assistant snapshots.
import type { AssistantMessage } from "openclaw/plugin-sdk/llm";
import { describe, expect, it, vi } from "vitest";
import {
@@ -85,6 +87,8 @@ describe("subscribeEmbeddedAgentSession", () => {
expect(subscription.assistantTexts).toEqual(["Hello world"]);
});
it("populates assistantTexts for non-streaming models with chunking enabled", () => {
// Non-streaming providers may only send message_end; assistantTexts still
// needs the final visible reply even when block chunking is enabled.
// Non-streaming models (e.g. zai/glm-4.7): no text_delta events; message_end
// must still populate assistantTexts so providers can deliver a final reply.
const { session, emit } = createStubSessionHarness();
@@ -1,3 +1,5 @@
// Text-end block reply tests cover streamed block delivery, message_end
// de-duplication, and OpenAI Responses phase handling.
import type { AssistantMessage } from "openclaw/plugin-sdk/llm";
import { describe, expect, it, vi } from "vitest";
import {
@@ -24,6 +26,8 @@ function emitOpenAiResponsesTextEvent(params: {
signaturePhase?: OpenAiResponsesTextEventPhase;
partialPhase?: OpenAiResponsesTextEventPhase;
}) {
// Responses events carry item ids and phase signatures; tests preserve those
// fields so commentary/final routing matches provider payloads.
const { emit, ...eventParams } = params;
emit(createOpenAiResponsesTextEvent(eventParams));
}
@@ -80,6 +84,8 @@ async function emitSuppressedCommentary(params: {
emit: TextEndBlockReplyHarness["emit"];
text: string;
}) {
// Commentary can stream before final_answer; this helper proves suppressed
// commentary does not count as a delivered block.
params.emit({ type: "message_start", message: { role: "assistant" } });
emitOpenAiResponsesTextDeltaAndEnd({
emit: params.emit,
@@ -101,6 +107,7 @@ function expectSingleBlockReplyText(params: {
}
function requireBlockReplyPayload(onBlockReply: OnBlockReplyMock): BlockReplyPayload {
// Most cases expect exactly one user-visible block reply.
const call = onBlockReply.mock.calls[0];
if (!call) {
throw new Error("expected first block reply call");
@@ -1,3 +1,5 @@
// Reasoning block reply tests cover message_end extraction of native reasoning
// and tag-promoted thinking when reasoning output is enabled.
import type { AssistantMessage } from "openclaw/plugin-sdk/llm";
import { describe, expect, it, vi } from "vitest";
import {
@@ -9,6 +11,8 @@ import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js";
describe("subscribeEmbeddedAgentSession", () => {
function createReasoningBlockReplyHarness(params: { thinkingLevel?: "off" | "medium" } = {}) {
// message_end block replies let reasoning and final answer be emitted as
// separate payloads without depending on streaming deltas.
const { session, emit } = createStubSessionHarness();
const onBlockReply = vi.fn();
@@ -33,6 +37,8 @@ describe("subscribeEmbeddedAgentSession", () => {
}
function expectReasoningAndAnswerCalls(onBlockReply: ReturnType<typeof vi.fn>) {
// The expected contract is two user-visible payloads: reasoning first,
// final answer second.
expect(onBlockReply).toHaveBeenCalledTimes(2);
expect(blockReplyTextAt(onBlockReply, 0)).toBe("Because it helps");
expect(blockReplyTextAt(onBlockReply, 1)).toBe("Final answer");
@@ -1,3 +1,5 @@
// Final-tag enforcement tests cover suppression of leaked reasoning and safe
// extraction of <final> content across streamed code/fence boundaries.
import type { AssistantMessage } from "openclaw/plugin-sdk/llm";
import { describe, expect, it, vi } from "vitest";
import {
@@ -13,6 +15,8 @@ type ReplyMock = ReturnType<typeof vi.fn>;
type ReplyPayload = { text?: string };
function requireFirstReplyPayload(mock: ReplyMock): ReplyPayload {
// Enforcement cases emit at most one visible reply before assertions inspect
// whether the hidden prefix stayed suppressed.
const call = mock.mock.calls[0];
if (!call) {
throw new Error("expected first reply call");
@@ -96,6 +100,8 @@ describe("subscribeEmbeddedAgentSession", () => {
});
it("keeps final tag literals inside streamed fenced code while enforcement is on", () => {
// Literal <final> tags inside code fences remain hidden until a real final
// tag appears outside the fence.
const { session, emit } = createStubSessionHarness();
const onAgentEvent = vi.fn();