| 59 | * previous behaviour (fresh `threadId`, no `parentRunId`). |
| 60 | */ |
| 61 | export async function* processConverseStream( |
| 62 | stream: AsyncIterable<ConverseStreamOutput>, |
| 63 | newMessageId: () => string, |
| 64 | lifecycle: { threadId?: string; parentRunId?: string; model?: string } = {}, |
| 65 | ): AsyncIterable<StreamChunk> { |
| 66 | const runId = newMessageId() |
| 67 | const threadId = lifecycle.threadId ?? newMessageId() |
| 68 | const { parentRunId, model } = lifecycle |
| 69 | const messageId = newMessageId() |
| 70 | |
| 71 | let hasEmittedRunStarted = false |
| 72 | |
| 73 | // Text lifecycle |
| 74 | let accumulatedContent = '' |
| 75 | let hasEmittedTextMessageStart = false |
| 76 | |
| 77 | // Reasoning lifecycle |
| 78 | let reasoningMessageId: string | undefined |
| 79 | let hasClosedReasoning = false |
| 80 | |
| 81 | // Tool-call lifecycle, keyed by Converse contentBlockIndex. Converse opens a |
| 82 | // tool-use block with `contentBlockStart`, streams arg fragments via |
| 83 | // `contentBlockDelta`, and closes it with `contentBlockStop`. |
| 84 | const toolCallsByIndex = new Map< |
| 85 | number, |
| 86 | { id: string; name: string; started: boolean } |
| 87 | >() |
| 88 | |
| 89 | // Usage + finish-reason are captured during iteration and folded into the |
| 90 | // single terminal RUN_FINISHED, matching openai-base's deferred-finish |
| 91 | // contract (usage may arrive after the finish signal). |
| 92 | let usage: |
| 93 | | { promptTokens: number; completionTokens: number; totalTokens: number } |
| 94 | | undefined |
| 95 | let finishReason: NonNullable<RunFinishedEvent['finishReason']> | undefined |
| 96 | |
| 97 | // Lazily emit RUN_STARTED exactly once, before the first content event. |
| 98 | function* ensureRunStarted(): Generator<StreamChunk> { |
| 99 | if (hasEmittedRunStarted) return |
| 100 | hasEmittedRunStarted = true |
| 101 | yield { |
| 102 | type: EventType.RUN_STARTED, |
| 103 | runId, |
| 104 | threadId, |
| 105 | parentRunId, |
| 106 | ...(model && { model }), |
| 107 | timestamp: Date.now(), |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // Close an open reasoning message before text/tool content begins, mirroring |
| 112 | // openai-base which always emits REASONING_MESSAGE_END before TEXT_MESSAGE_START. |
| 113 | function* closeReasoning(): Generator<StreamChunk> { |
| 114 | if (reasoningMessageId && !hasClosedReasoning) { |
| 115 | hasClosedReasoning = true |
| 116 | yield { |
| 117 | type: EventType.REASONING_MESSAGE_END, |
| 118 | messageId: reasoningMessageId, |