Skip to content

Commit 8030a6c

Browse files
authored
Emit LLM stream lifecycle events (#26971)
1 parent e5aa516 commit 8030a6c

14 files changed

Lines changed: 560 additions & 196 deletions

packages/llm/src/protocols/anthropic-messages.ts

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "../schema"
1818
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
1919
import * as Cache from "./utils/cache"
20+
import { Lifecycle } from "./utils/lifecycle"
2021
import { ToolStream } from "./utils/tool-stream"
2122

2223
const ADAPTER = "anthropic-messages"
@@ -190,6 +191,7 @@ type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
190191
interface ParserState {
191192
readonly tools: ToolStream.State<number>
192193
readonly usage?: Usage
194+
readonly lifecycle: Lifecycle.State
193195
}
194196

195197
const invalid = ProviderShared.invalidRequest
@@ -500,37 +502,45 @@ const onContentBlockStart = (state: ParserState, event: AnthropicEvent): StepRes
500502
if (!block) return [state, NO_EVENTS]
501503

502504
if ((block.type === "tool_use" || block.type === "server_tool_use") && event.index !== undefined) {
505+
const events: LLMEvent[] = []
506+
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
503507
return [
504508
{
505509
...state,
510+
lifecycle,
506511
tools: ToolStream.start(state.tools, event.index, {
507512
id: block.id ?? String(event.index),
508513
name: block.name ?? "",
509514
providerExecuted: block.type === "server_tool_use",
510515
}),
511516
},
512-
NO_EVENTS,
517+
[...events, LLMEvent.toolInputStart({ id: block.id ?? String(event.index), name: block.name ?? "" })],
513518
]
514519
}
515520

516521
if (block.type === "text" && block.text) {
517-
return [state, [LLMEvent.textDelta({ id: `text-${event.index ?? 0}`, text: block.text })]]
522+
const events: LLMEvent[] = []
523+
return [
524+
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, block.text) },
525+
events,
526+
]
518527
}
519528

520529
if (block.type === "thinking" && block.thinking) {
530+
const events: LLMEvent[] = []
521531
return [
522-
state,
523-
[
524-
LLMEvent.reasoningDelta({
525-
id: `reasoning-${event.index ?? 0}`,
526-
text: block.thinking,
527-
}),
528-
],
532+
{
533+
...state,
534+
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, block.thinking),
535+
},
536+
events,
529537
]
530538
}
531539

532540
const result = serverToolResultEvent(block)
533-
return [state, result ? [result] : NO_EVENTS]
541+
if (!result) return [state, NO_EVENTS]
542+
const events: LLMEvent[] = []
543+
return [{ ...state, lifecycle: Lifecycle.stepStart(state.lifecycle, events) }, [...events, result]]
534544
}
535545

536546
const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(function* (
@@ -540,25 +550,37 @@ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(f
540550
const delta = event.delta
541551

542552
if (delta?.type === "text_delta" && delta.text) {
543-
return [state, [LLMEvent.textDelta({ id: `text-${event.index ?? 0}`, text: delta.text })]] satisfies StepResult
553+
const events: LLMEvent[] = []
554+
return [
555+
{ ...state, lifecycle: Lifecycle.textDelta(state.lifecycle, events, `text-${event.index ?? 0}`, delta.text) },
556+
events,
557+
] satisfies StepResult
544558
}
545559

546560
if (delta?.type === "thinking_delta" && delta.thinking) {
561+
const events: LLMEvent[] = []
547562
return [
548-
state,
549-
[LLMEvent.reasoningDelta({ id: `reasoning-${event.index ?? 0}`, text: delta.thinking })],
563+
{
564+
...state,
565+
lifecycle: Lifecycle.reasoningDelta(state.lifecycle, events, `reasoning-${event.index ?? 0}`, delta.thinking),
566+
},
567+
events,
550568
] satisfies StepResult
551569
}
552570

553571
if (delta?.type === "signature_delta" && delta.signature) {
572+
const events: LLMEvent[] = []
554573
return [
555-
state,
556-
[
557-
LLMEvent.reasoningEnd({
558-
id: `reasoning-${event.index ?? 0}`,
559-
providerMetadata: anthropicMetadata({ signature: delta.signature }),
560-
}),
561-
],
574+
{
575+
...state,
576+
lifecycle: Lifecycle.reasoningEnd(
577+
state.lifecycle,
578+
events,
579+
`reasoning-${event.index ?? 0}`,
580+
anthropicMetadata({ signature: delta.signature }),
581+
),
582+
},
583+
events,
562584
] satisfies StepResult
563585
}
564586

@@ -572,7 +594,10 @@ const onContentBlockDelta = Effect.fn("AnthropicMessages.onContentBlockDelta")(f
572594
"Anthropic Messages tool argument delta is missing its tool call",
573595
)
574596
if (ToolStream.isError(result)) return yield* result
575-
return [{ ...state, tools: result.tools }, result.event ? [result.event] : NO_EVENTS] satisfies StepResult
597+
const events: LLMEvent[] = []
598+
const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
599+
events.push(...result.events)
600+
return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
576601
}
577602

578603
return [state, NO_EVENTS] satisfies StepResult
@@ -584,23 +609,30 @@ const onContentBlockStop = Effect.fn("AnthropicMessages.onContentBlockStop")(fun
584609
) {
585610
if (event.index === undefined) return [state, NO_EVENTS] satisfies StepResult
586611
const result = yield* ToolStream.finish(ADAPTER, state.tools, event.index)
587-
return [{ ...state, tools: result.tools }, result.event ? [result.event] : NO_EVENTS] satisfies StepResult
612+
const events: LLMEvent[] = []
613+
const resultEvents = result.events ?? []
614+
const lifecycle = resultEvents.length
615+
? Lifecycle.stepStart(state.lifecycle, events)
616+
: Lifecycle.reasoningEnd(
617+
Lifecycle.textEnd(state.lifecycle, events, `text-${event.index}`),
618+
events,
619+
`reasoning-${event.index}`,
620+
)
621+
events.push(...resultEvents)
622+
return [{ ...state, lifecycle, tools: result.tools }, events] satisfies StepResult
588623
})
589624

590625
const onMessageDelta = (state: ParserState, event: AnthropicEvent): StepResult => {
591626
const usage = mergeUsage(state.usage, mapUsage(event.usage))
592-
return [
593-
{ ...state, usage },
594-
[
595-
LLMEvent.requestFinish({
596-
reason: mapFinishReason(event.delta?.stop_reason),
597-
usage,
598-
providerMetadata: event.delta?.stop_sequence
599-
? anthropicMetadata({ stopSequence: event.delta.stop_sequence })
600-
: undefined,
601-
}),
602-
],
603-
]
627+
const events: LLMEvent[] = []
628+
const lifecycle = Lifecycle.finish(state.lifecycle, events, {
629+
reason: mapFinishReason(event.delta?.stop_reason),
630+
usage,
631+
providerMetadata: event.delta?.stop_sequence
632+
? anthropicMetadata({ stopSequence: event.delta.stop_sequence })
633+
: undefined,
634+
})
635+
return [{ ...state, lifecycle, usage }, events]
604636
}
605637

606638
const onError = (state: ParserState, event: AnthropicEvent): StepResult => [
@@ -634,7 +666,7 @@ export const protocol = Protocol.make({
634666
},
635667
stream: {
636668
event: Protocol.jsonEvent(AnthropicEvent),
637-
initial: () => ({ tools: ToolStream.empty<number>() }),
669+
initial: () => ({ tools: ToolStream.empty<number>(), lifecycle: Lifecycle.initial() }),
638670
step,
639671
},
640672
})

packages/llm/src/protocols/bedrock-converse.ts

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { JsonObject, optionalArray, ProviderShared } from "./shared"
1717
import { BedrockAuth, type Credentials as BedrockCredentials } from "./utils/bedrock-auth"
1818
import { BedrockCache } from "./utils/bedrock-cache"
1919
import { BedrockMedia } from "./utils/bedrock-media"
20+
import { Lifecycle } from "./utils/lifecycle"
2021
import { ToolStream } from "./utils/tool-stream"
2122

2223
const ADAPTER = "bedrock-converse"
@@ -420,45 +421,64 @@ interface ParserState {
420421
// `metadata` (carries usage). Hold the terminal event in state so `onHalt`
421422
// can emit exactly one finish after both chunks have had a chance to arrive.
422423
readonly pendingFinish: { readonly reason: FinishReason; readonly usage?: Usage } | undefined
424+
readonly hasToolCalls: boolean
425+
readonly lifecycle: Lifecycle.State
423426
}
424427

425428
const step = (state: ParserState, event: BedrockEvent) =>
426429
Effect.gen(function* () {
427430
if (event.contentBlockStart?.start?.toolUse) {
428431
const index = event.contentBlockStart.contentBlockIndex
432+
const events: LLMEvent[] = []
433+
const lifecycle = Lifecycle.stepStart(state.lifecycle, events)
429434
return [
430435
{
431436
...state,
437+
lifecycle,
432438
tools: ToolStream.start(state.tools, index, {
433439
id: event.contentBlockStart.start.toolUse.toolUseId,
434440
name: event.contentBlockStart.start.toolUse.name,
435441
}),
436442
},
437-
[],
443+
[
444+
...events,
445+
LLMEvent.toolInputStart({
446+
id: event.contentBlockStart.start.toolUse.toolUseId,
447+
name: event.contentBlockStart.start.toolUse.name,
448+
}),
449+
],
438450
] as const
439451
}
440452

441453
if (event.contentBlockDelta?.delta?.text) {
454+
const events: LLMEvent[] = []
442455
return [
443-
state,
444-
[
445-
LLMEvent.textDelta({
446-
id: `text-${event.contentBlockDelta.contentBlockIndex}`,
447-
text: event.contentBlockDelta.delta.text,
448-
}),
449-
],
456+
{
457+
...state,
458+
lifecycle: Lifecycle.textDelta(
459+
state.lifecycle,
460+
events,
461+
`text-${event.contentBlockDelta.contentBlockIndex}`,
462+
event.contentBlockDelta.delta.text,
463+
),
464+
},
465+
events,
450466
] as const
451467
}
452468

453469
if (event.contentBlockDelta?.delta?.reasoningContent?.text) {
470+
const events: LLMEvent[] = []
454471
return [
455-
state,
456-
[
457-
LLMEvent.reasoningDelta({
458-
id: `reasoning-${event.contentBlockDelta.contentBlockIndex}`,
459-
text: event.contentBlockDelta.delta.reasoningContent.text,
460-
}),
461-
],
472+
{
473+
...state,
474+
lifecycle: Lifecycle.reasoningDelta(
475+
state.lifecycle,
476+
events,
477+
`reasoning-${event.contentBlockDelta.contentBlockIndex}`,
478+
event.contentBlockDelta.delta.reasoningContent.text,
479+
),
480+
},
481+
events,
462482
] as const
463483
}
464484

@@ -472,12 +492,33 @@ const step = (state: ParserState, event: BedrockEvent) =>
472492
"Bedrock Converse tool delta is missing its tool call",
473493
)
474494
if (ToolStream.isError(result)) return yield* result
475-
return [{ ...state, tools: result.tools }, result.event ? [result.event] : []] as const
495+
const events: LLMEvent[] = []
496+
const lifecycle = result.events.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle
497+
events.push(...result.events)
498+
return [{ ...state, lifecycle, tools: result.tools }, events] as const
476499
}
477500

478501
if (event.contentBlockStop) {
479502
const result = yield* ToolStream.finish(ADAPTER, state.tools, event.contentBlockStop.contentBlockIndex)
480-
return [{ ...state, tools: result.tools }, result.event ? [result.event] : []] as const
503+
const events: LLMEvent[] = []
504+
const resultEvents = result.events ?? []
505+
const lifecycle = resultEvents.length
506+
? Lifecycle.stepStart(state.lifecycle, events)
507+
: Lifecycle.reasoningEnd(
508+
Lifecycle.textEnd(state.lifecycle, events, `text-${event.contentBlockStop.contentBlockIndex}`),
509+
events,
510+
`reasoning-${event.contentBlockStop.contentBlockIndex}`,
511+
)
512+
events.push(...resultEvents)
513+
return [
514+
{
515+
...state,
516+
hasToolCalls: resultEvents.some(LLMEvent.is.toolCall) ? true : state.hasToolCalls,
517+
lifecycle,
518+
tools: result.tools,
519+
},
520+
events,
521+
] as const
481522
}
482523

483524
if (event.messageStop) {
@@ -517,7 +558,15 @@ const framing = BedrockEventStream.framing(ADAPTER)
517558

518559
const onHalt = (state: ParserState): ReadonlyArray<LLMEvent> =>
519560
state.pendingFinish
520-
? [LLMEvent.requestFinish({ reason: state.pendingFinish.reason, usage: state.pendingFinish.usage })]
561+
? (() => {
562+
const events: LLMEvent[] = []
563+
Lifecycle.finish(state.lifecycle, events, {
564+
reason:
565+
state.pendingFinish.reason === "stop" && state.hasToolCalls ? "tool-calls" : state.pendingFinish.reason,
566+
usage: state.pendingFinish.usage,
567+
})
568+
return events
569+
})()
521570
: []
522571

523572
// =============================================================================
@@ -535,7 +584,12 @@ export const protocol = Protocol.make({
535584
},
536585
stream: {
537586
event: BedrockEvent,
538-
initial: () => ({ tools: ToolStream.empty<number>(), pendingFinish: undefined }),
587+
initial: () => ({
588+
tools: ToolStream.empty<number>(),
589+
pendingFinish: undefined,
590+
hasToolCalls: false,
591+
lifecycle: Lifecycle.initial(),
592+
}),
539593
step,
540594
onHalt,
541595
},

0 commit comments

Comments
 (0)