mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
openspec/lightweight-tasks/task1-2-3.md
feat: add StreamJsonWriter and associated types for structured JSON streaming
This commit is contained in:
183
packages/cli/src/streamJson/types.ts
Normal file
183
packages/cli/src/streamJson/types.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
export type StreamJsonFormat = 'text' | 'stream-json';
|
||||
|
||||
export interface StreamJsonAnnotation {
|
||||
type: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface StreamJsonTextBlock {
|
||||
type: 'text';
|
||||
text: string;
|
||||
annotations?: StreamJsonAnnotation[];
|
||||
}
|
||||
|
||||
export interface StreamJsonThinkingBlock {
|
||||
type: 'thinking';
|
||||
thinking: string;
|
||||
signature?: string;
|
||||
annotations?: StreamJsonAnnotation[];
|
||||
}
|
||||
|
||||
export interface StreamJsonToolUseBlock {
|
||||
type: 'tool_use';
|
||||
id: string;
|
||||
name: string;
|
||||
input: unknown;
|
||||
annotations?: StreamJsonAnnotation[];
|
||||
}
|
||||
|
||||
export interface StreamJsonToolResultBlock {
|
||||
type: 'tool_result';
|
||||
tool_use_id: string;
|
||||
content?: StreamJsonContentBlock[] | string;
|
||||
is_error?: boolean;
|
||||
annotations?: StreamJsonAnnotation[];
|
||||
}
|
||||
|
||||
export type StreamJsonContentBlock =
|
||||
| StreamJsonTextBlock
|
||||
| StreamJsonThinkingBlock
|
||||
| StreamJsonToolUseBlock
|
||||
| StreamJsonToolResultBlock;
|
||||
|
||||
export interface StreamJsonAssistantEnvelope {
|
||||
type: 'assistant';
|
||||
message: {
|
||||
role: 'assistant';
|
||||
model?: string;
|
||||
content: StreamJsonContentBlock[];
|
||||
};
|
||||
parent_tool_use_id?: string;
|
||||
}
|
||||
|
||||
export interface StreamJsonUserEnvelope {
|
||||
type: 'user';
|
||||
message: {
|
||||
role?: 'user';
|
||||
content: string | StreamJsonContentBlock[];
|
||||
};
|
||||
parent_tool_use_id?: string;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface StreamJsonSystemEnvelope {
|
||||
type: 'system';
|
||||
subtype?: string;
|
||||
session_id?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
|
||||
export interface StreamJsonUsage {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
total_tokens?: number;
|
||||
cache_creation_input_tokens?: number;
|
||||
cache_read_input_tokens?: number;
|
||||
}
|
||||
|
||||
export interface StreamJsonResultEnvelope {
|
||||
type: 'result';
|
||||
subtype?: string;
|
||||
duration_ms?: number;
|
||||
duration_api_ms?: number;
|
||||
num_turns?: number;
|
||||
session_id?: string;
|
||||
is_error?: boolean;
|
||||
summary?: string;
|
||||
usage?: StreamJsonUsage;
|
||||
total_cost_usd?: number;
|
||||
error?: { type?: string; message: string; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface StreamJsonMessageStreamEvent {
|
||||
type: string;
|
||||
index?: number;
|
||||
delta?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface StreamJsonStreamEventEnvelope {
|
||||
type: 'stream_event';
|
||||
uuid: string;
|
||||
session_id?: string;
|
||||
event: StreamJsonMessageStreamEvent;
|
||||
}
|
||||
|
||||
export interface StreamJsonControlRequestEnvelope {
|
||||
type: 'control_request';
|
||||
request_id: string;
|
||||
request: {
|
||||
subtype: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StreamJsonControlResponseEnvelope {
|
||||
type: 'control_response';
|
||||
request_id: string;
|
||||
success?: boolean;
|
||||
response?: unknown;
|
||||
error?: string | { message: string; [key: string]: unknown };
|
||||
}
|
||||
|
||||
export interface StreamJsonControlCancelRequestEnvelope {
|
||||
type: 'control_cancel_request';
|
||||
request_id?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export type StreamJsonOutputEnvelope =
|
||||
| StreamJsonAssistantEnvelope
|
||||
| StreamJsonUserEnvelope
|
||||
| StreamJsonSystemEnvelope
|
||||
| StreamJsonResultEnvelope
|
||||
| StreamJsonStreamEventEnvelope
|
||||
| StreamJsonControlRequestEnvelope
|
||||
| StreamJsonControlResponseEnvelope
|
||||
| StreamJsonControlCancelRequestEnvelope;
|
||||
|
||||
export type StreamJsonInputEnvelope =
|
||||
| StreamJsonUserEnvelope
|
||||
| StreamJsonControlRequestEnvelope
|
||||
| StreamJsonControlResponseEnvelope
|
||||
| StreamJsonControlCancelRequestEnvelope;
|
||||
|
||||
export type StreamJsonEnvelope =
|
||||
| StreamJsonOutputEnvelope
|
||||
| StreamJsonInputEnvelope;
|
||||
|
||||
export function serializeStreamJsonEnvelope(
|
||||
envelope: StreamJsonOutputEnvelope,
|
||||
): string {
|
||||
return JSON.stringify(envelope);
|
||||
}
|
||||
|
||||
export class StreamJsonParseError extends Error {}
|
||||
|
||||
export function parseStreamJsonEnvelope(line: string): StreamJsonEnvelope {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line) as StreamJsonEnvelope;
|
||||
} catch (error) {
|
||||
throw new StreamJsonParseError(
|
||||
`Failed to parse stream-json line: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
throw new StreamJsonParseError('Parsed value is not an object');
|
||||
}
|
||||
const type = (parsed as { type?: unknown }).type;
|
||||
if (typeof type !== 'string') {
|
||||
throw new StreamJsonParseError('Missing required "type" field');
|
||||
}
|
||||
return parsed as StreamJsonEnvelope;
|
||||
}
|
||||
146
packages/cli/src/streamJson/writer.test.ts
Normal file
146
packages/cli/src/streamJson/writer.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Config, ToolCallRequestInfo } from '@qwen-code/qwen-code-core';
|
||||
import { StreamJsonWriter } from './writer.js';
|
||||
|
||||
function createConfig(): Config {
|
||||
return {
|
||||
getSessionId: () => 'session-test',
|
||||
getModel: () => 'model-test',
|
||||
} as unknown as Config;
|
||||
}
|
||||
|
||||
function parseEnvelopes(writes: string[]): unknown[] {
|
||||
return writes
|
||||
.join('')
|
||||
.split('\n')
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
describe('StreamJsonWriter', () => {
|
||||
let writes: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
writes = [];
|
||||
vi.spyOn(process.stdout, 'write').mockImplementation(
|
||||
(chunk: string | Uint8Array) => {
|
||||
if (typeof chunk === 'string') {
|
||||
writes.push(chunk);
|
||||
} else {
|
||||
writes.push(Buffer.from(chunk).toString('utf8'));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('emits result envelopes with usage and cost details', () => {
|
||||
const writer = new StreamJsonWriter(createConfig(), false);
|
||||
writer.emitResult({
|
||||
isError: false,
|
||||
numTurns: 2,
|
||||
durationMs: 1200,
|
||||
apiDurationMs: 800,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
cache_read_input_tokens: 2,
|
||||
},
|
||||
totalCostUsd: 0.123,
|
||||
summary: 'Completed',
|
||||
subtype: 'session_summary',
|
||||
});
|
||||
|
||||
const [envelope] = parseEnvelopes(writes) as Array<Record<string, unknown>>;
|
||||
expect(envelope).toMatchObject({
|
||||
type: 'result',
|
||||
duration_ms: 1200,
|
||||
duration_api_ms: 800,
|
||||
usage: {
|
||||
input_tokens: 10,
|
||||
output_tokens: 5,
|
||||
total_tokens: 15,
|
||||
cache_read_input_tokens: 2,
|
||||
},
|
||||
total_cost_usd: 0.123,
|
||||
summary: 'Completed',
|
||||
subtype: 'session_summary',
|
||||
is_error: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits thinking deltas and assistant messages for thought blocks', () => {
|
||||
const writer = new StreamJsonWriter(createConfig(), true);
|
||||
const builder = writer.createAssistantBuilder();
|
||||
builder.appendThinking('Reflecting');
|
||||
builder.appendThinking(' more');
|
||||
builder.finalize();
|
||||
|
||||
const envelopes = parseEnvelopes(writes) as Array<Record<string, unknown>>;
|
||||
|
||||
expect(
|
||||
envelopes.some(
|
||||
(env) =>
|
||||
env.type === 'stream_event' &&
|
||||
env.event?.type === 'content_block_delta' &&
|
||||
env.event?.delta?.type === 'thinking_delta',
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const assistantEnvelope = envelopes.find((env) => env.type === 'assistant');
|
||||
expect(assistantEnvelope?.message?.content?.[0]).toEqual({
|
||||
type: 'thinking',
|
||||
thinking: 'Reflecting more',
|
||||
});
|
||||
});
|
||||
|
||||
it('emits input_json_delta events when tool calls are appended', () => {
|
||||
const writer = new StreamJsonWriter(createConfig(), true);
|
||||
const builder = writer.createAssistantBuilder();
|
||||
const request: ToolCallRequestInfo = {
|
||||
callId: 'tool-123',
|
||||
name: 'write_file',
|
||||
args: { path: 'foo.ts', content: 'console.log(1);' },
|
||||
isClientInitiated: false,
|
||||
prompt_id: 'prompt-1',
|
||||
};
|
||||
|
||||
builder.appendToolUse(request);
|
||||
builder.finalize();
|
||||
|
||||
const envelopes = parseEnvelopes(writes) as Array<Record<string, unknown>>;
|
||||
|
||||
expect(
|
||||
envelopes.some(
|
||||
(env) =>
|
||||
env.type === 'stream_event' &&
|
||||
env.event?.type === 'content_block_delta' &&
|
||||
env.event?.delta?.type === 'input_json_delta',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('includes session id in system messages', () => {
|
||||
const writer = new StreamJsonWriter(createConfig(), false);
|
||||
writer.emitSystemMessage('init', { foo: 'bar' });
|
||||
|
||||
const [envelope] = parseEnvelopes(writes) as Array<Record<string, unknown>>;
|
||||
expect(envelope).toMatchObject({
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
session_id: 'session-test',
|
||||
data: { foo: 'bar' },
|
||||
});
|
||||
});
|
||||
});
|
||||
357
packages/cli/src/streamJson/writer.ts
Normal file
357
packages/cli/src/streamJson/writer.ts
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type {
|
||||
Config,
|
||||
ToolCallRequestInfo,
|
||||
ToolCallResponseInfo,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import type { Part } from '@google/genai';
|
||||
import {
|
||||
serializeStreamJsonEnvelope,
|
||||
type StreamJsonAssistantEnvelope,
|
||||
type StreamJsonContentBlock,
|
||||
type StreamJsonMessageStreamEvent,
|
||||
type StreamJsonOutputEnvelope,
|
||||
type StreamJsonStreamEventEnvelope,
|
||||
type StreamJsonUsage,
|
||||
type StreamJsonToolResultBlock,
|
||||
} from './types.js';
|
||||
|
||||
export interface StreamJsonResultOptions {
|
||||
readonly isError: boolean;
|
||||
readonly errorMessage?: string;
|
||||
readonly durationMs?: number;
|
||||
readonly apiDurationMs?: number;
|
||||
readonly numTurns: number;
|
||||
readonly usage?: StreamJsonUsage;
|
||||
readonly totalCostUsd?: number;
|
||||
readonly summary?: string;
|
||||
readonly subtype?: string;
|
||||
}
|
||||
|
||||
export class StreamJsonWriter {
|
||||
private readonly includePartialMessages: boolean;
|
||||
private readonly sessionId: string;
|
||||
private readonly model: string;
|
||||
|
||||
constructor(config: Config, includePartialMessages: boolean) {
|
||||
this.includePartialMessages = includePartialMessages;
|
||||
this.sessionId = config.getSessionId();
|
||||
this.model = config.getModel();
|
||||
}
|
||||
|
||||
createAssistantBuilder(): StreamJsonAssistantMessageBuilder {
|
||||
return new StreamJsonAssistantMessageBuilder(
|
||||
this,
|
||||
this.includePartialMessages,
|
||||
this.sessionId,
|
||||
this.model,
|
||||
);
|
||||
}
|
||||
|
||||
emitUserMessageFromParts(parts: Part[], parentToolUseId?: string): void {
|
||||
const envelope: StreamJsonOutputEnvelope = {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: this.partsToString(parts),
|
||||
},
|
||||
parent_tool_use_id: parentToolUseId,
|
||||
};
|
||||
this.writeEnvelope(envelope);
|
||||
}
|
||||
|
||||
emitToolResult(
|
||||
request: ToolCallRequestInfo,
|
||||
response: ToolCallResponseInfo,
|
||||
): void {
|
||||
const block: StreamJsonToolResultBlock = {
|
||||
type: 'tool_result',
|
||||
tool_use_id: request.callId,
|
||||
is_error: Boolean(response.error),
|
||||
};
|
||||
const content = this.toolResultContent(response);
|
||||
if (content !== undefined) {
|
||||
block.content = content;
|
||||
}
|
||||
|
||||
const envelope: StreamJsonOutputEnvelope = {
|
||||
type: 'user',
|
||||
message: {
|
||||
content: [block],
|
||||
},
|
||||
parent_tool_use_id: request.callId,
|
||||
};
|
||||
this.writeEnvelope(envelope);
|
||||
}
|
||||
|
||||
emitResult(options: StreamJsonResultOptions): void {
|
||||
const envelope: StreamJsonOutputEnvelope = {
|
||||
type: 'result',
|
||||
subtype:
|
||||
options.subtype ?? (options.isError ? 'error' : 'session_summary'),
|
||||
is_error: options.isError,
|
||||
session_id: this.sessionId,
|
||||
num_turns: options.numTurns,
|
||||
};
|
||||
|
||||
if (typeof options.durationMs === 'number') {
|
||||
envelope.duration_ms = options.durationMs;
|
||||
}
|
||||
if (typeof options.apiDurationMs === 'number') {
|
||||
envelope.duration_api_ms = options.apiDurationMs;
|
||||
}
|
||||
if (options.summary) {
|
||||
envelope.summary = options.summary;
|
||||
}
|
||||
if (options.usage) {
|
||||
envelope.usage = options.usage;
|
||||
}
|
||||
if (typeof options.totalCostUsd === 'number') {
|
||||
envelope.total_cost_usd = options.totalCostUsd;
|
||||
}
|
||||
if (options.errorMessage) {
|
||||
envelope.error = { message: options.errorMessage };
|
||||
}
|
||||
|
||||
this.writeEnvelope(envelope);
|
||||
}
|
||||
|
||||
emitSystemMessage(subtype: string, data?: unknown): void {
|
||||
const envelope: StreamJsonOutputEnvelope = {
|
||||
type: 'system',
|
||||
subtype,
|
||||
session_id: this.sessionId,
|
||||
data,
|
||||
};
|
||||
this.writeEnvelope(envelope);
|
||||
}
|
||||
|
||||
emitStreamEvent(event: StreamJsonMessageStreamEvent): void {
|
||||
if (!this.includePartialMessages) {
|
||||
return;
|
||||
}
|
||||
const envelope: StreamJsonStreamEventEnvelope = {
|
||||
type: 'stream_event',
|
||||
uuid: randomUUID(),
|
||||
session_id: this.sessionId,
|
||||
event,
|
||||
};
|
||||
this.writeEnvelope(envelope);
|
||||
}
|
||||
|
||||
writeEnvelope(envelope: StreamJsonOutputEnvelope): void {
|
||||
const line = serializeStreamJsonEnvelope(envelope);
|
||||
process.stdout.write(`${line}\n`);
|
||||
}
|
||||
|
||||
private toolResultContent(
|
||||
response: ToolCallResponseInfo,
|
||||
): string | undefined {
|
||||
if (typeof response.resultDisplay === 'string') {
|
||||
return response.resultDisplay;
|
||||
}
|
||||
if (response.responseParts && response.responseParts.length > 0) {
|
||||
return this.partsToString(response.responseParts);
|
||||
}
|
||||
if (response.error) {
|
||||
return response.error.message;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private partsToString(parts: Part[]): string {
|
||||
return parts
|
||||
.map((part) => {
|
||||
if ('text' in part && typeof part.text === 'string') {
|
||||
return part.text;
|
||||
}
|
||||
return JSON.stringify(part);
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
class StreamJsonAssistantMessageBuilder {
|
||||
private readonly blocks: StreamJsonContentBlock[] = [];
|
||||
private readonly openBlocks = new Set<number>();
|
||||
private started = false;
|
||||
private finalized = false;
|
||||
private messageId: string | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly writer: StreamJsonWriter,
|
||||
private readonly includePartialMessages: boolean,
|
||||
private readonly sessionId: string,
|
||||
private readonly model: string,
|
||||
) {}
|
||||
|
||||
appendText(fragment: string): void {
|
||||
if (this.finalized) {
|
||||
return;
|
||||
}
|
||||
this.ensureMessageStarted();
|
||||
|
||||
let currentBlock = this.blocks[this.blocks.length - 1];
|
||||
if (!currentBlock || currentBlock.type !== 'text') {
|
||||
currentBlock = { type: 'text', text: '' };
|
||||
const index = this.blocks.length;
|
||||
this.blocks.push(currentBlock);
|
||||
this.openBlock(index, currentBlock);
|
||||
}
|
||||
|
||||
currentBlock.text += fragment;
|
||||
const index = this.blocks.length - 1;
|
||||
this.emitEvent({
|
||||
type: 'content_block_delta',
|
||||
index,
|
||||
delta: { type: 'text_delta', text: fragment },
|
||||
});
|
||||
}
|
||||
|
||||
appendThinking(fragment: string): void {
|
||||
if (this.finalized) {
|
||||
return;
|
||||
}
|
||||
this.ensureMessageStarted();
|
||||
|
||||
let currentBlock = this.blocks[this.blocks.length - 1];
|
||||
if (!currentBlock || currentBlock.type !== 'thinking') {
|
||||
currentBlock = { type: 'thinking', thinking: '' };
|
||||
const index = this.blocks.length;
|
||||
this.blocks.push(currentBlock);
|
||||
this.openBlock(index, currentBlock);
|
||||
}
|
||||
|
||||
currentBlock.thinking = `${currentBlock.thinking ?? ''}${fragment}`;
|
||||
const index = this.blocks.length - 1;
|
||||
this.emitEvent({
|
||||
type: 'content_block_delta',
|
||||
index,
|
||||
delta: { type: 'thinking_delta', thinking: fragment },
|
||||
});
|
||||
}
|
||||
|
||||
appendToolUse(request: ToolCallRequestInfo): void {
|
||||
if (this.finalized) {
|
||||
return;
|
||||
}
|
||||
this.ensureMessageStarted();
|
||||
const index = this.blocks.length;
|
||||
const block: StreamJsonContentBlock = {
|
||||
type: 'tool_use',
|
||||
id: request.callId,
|
||||
name: request.name,
|
||||
input: request.args,
|
||||
};
|
||||
this.blocks.push(block);
|
||||
this.openBlock(index, block);
|
||||
this.emitEvent({
|
||||
type: 'content_block_delta',
|
||||
index,
|
||||
delta: {
|
||||
type: 'input_json_delta',
|
||||
partial_json: JSON.stringify(request.args ?? {}),
|
||||
},
|
||||
});
|
||||
this.closeBlock(index);
|
||||
}
|
||||
|
||||
finalize(): StreamJsonAssistantEnvelope {
|
||||
if (this.finalized) {
|
||||
return {
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
model: this.model,
|
||||
content: this.blocks,
|
||||
},
|
||||
};
|
||||
}
|
||||
this.finalized = true;
|
||||
|
||||
const orderedOpenBlocks = [...this.openBlocks].sort((a, b) => a - b);
|
||||
for (const index of orderedOpenBlocks) {
|
||||
this.closeBlock(index);
|
||||
}
|
||||
|
||||
if (this.includePartialMessages && this.started) {
|
||||
this.emitEvent({
|
||||
type: 'message_stop',
|
||||
message: {
|
||||
type: 'assistant',
|
||||
role: 'assistant',
|
||||
model: this.model,
|
||||
session_id: this.sessionId,
|
||||
id: this.messageId ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const envelope: StreamJsonAssistantEnvelope = {
|
||||
type: 'assistant',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
model: this.model,
|
||||
content: this.blocks,
|
||||
},
|
||||
};
|
||||
this.writer.writeEnvelope(envelope);
|
||||
return envelope;
|
||||
}
|
||||
|
||||
private ensureMessageStarted(): void {
|
||||
if (this.started) {
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
if (!this.messageId) {
|
||||
this.messageId = randomUUID();
|
||||
}
|
||||
this.emitEvent({
|
||||
type: 'message_start',
|
||||
message: {
|
||||
type: 'assistant',
|
||||
role: 'assistant',
|
||||
model: this.model,
|
||||
session_id: this.sessionId,
|
||||
id: this.messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private openBlock(index: number, block: StreamJsonContentBlock): void {
|
||||
this.openBlocks.add(index);
|
||||
this.emitEvent({
|
||||
type: 'content_block_start',
|
||||
index,
|
||||
content_block: block,
|
||||
});
|
||||
}
|
||||
|
||||
private closeBlock(index: number): void {
|
||||
if (!this.openBlocks.has(index)) {
|
||||
return;
|
||||
}
|
||||
this.openBlocks.delete(index);
|
||||
this.emitEvent({
|
||||
type: 'content_block_stop',
|
||||
index,
|
||||
});
|
||||
}
|
||||
|
||||
private emitEvent(event: StreamJsonMessageStreamEvent): void {
|
||||
if (!this.includePartialMessages) {
|
||||
return;
|
||||
}
|
||||
const enriched = this.messageId
|
||||
? { ...event, message_id: this.messageId }
|
||||
: event;
|
||||
this.writer.emitStreamEvent(enriched);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user