diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 1b87df4eb9..00b1686cd8 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -191,7 +191,7 @@ export const updateRequest = (input: LLMRequest, patch: Partial) = export const outputText = (response: LLMResponse | { readonly events: ReadonlyArray }) => response.events - .filter(LLMEvent.guards["text-delta"]) + .filter(LLMEvent.is.textDelta) .map((event) => event.text) .join("") @@ -204,10 +204,10 @@ export const outputUsage = (response: LLMResponse | { readonly events: ReadonlyA } export const outputToolCalls = (response: LLMResponse | { readonly events: ReadonlyArray }) => - response.events.filter(LLMEvent.guards["tool-call"]) + response.events.filter(LLMEvent.is.toolCall) export const outputReasoning = (response: LLMResponse | { readonly events: ReadonlyArray }) => response.events - .filter(LLMEvent.guards["reasoning-delta"]) + .filter(LLMEvent.is.reasoningDelta) .map((event) => event.text) .join("") diff --git a/packages/llm/src/schema.ts b/packages/llm/src/schema.ts index 3362bf6bb3..ab8bd626f8 100644 --- a/packages/llm/src/schema.ts +++ b/packages/llm/src/schema.ts @@ -338,7 +338,7 @@ export const ProviderErrorEvent = Schema.Struct({ }).annotate({ identifier: "LLM.Event.ProviderError" }) export type ProviderErrorEvent = Schema.Schema.Type -export const LLMEvent = Schema.Union([ +const llmEventTagged = Schema.Union([ RequestStart, StepStart, TextStart, @@ -353,7 +353,30 @@ export const LLMEvent = Schema.Union([ RequestFinish, ProviderErrorEvent, ]).pipe(Schema.toTaggedUnion("type")) -export type LLMEvent = Schema.Schema.Type + +/** + * camelCase aliases for `LLMEvent.guards` (provided by `Schema.toTaggedUnion`). + * Lets consumers write `events.filter(LLMEvent.is.toolCall)` instead of + * `events.filter(LLMEvent.guards["tool-call"])`. + */ +const llmEventIs = { + requestStart: llmEventTagged.guards["request-start"], + stepStart: llmEventTagged.guards["step-start"], + textStart: llmEventTagged.guards["text-start"], + textDelta: llmEventTagged.guards["text-delta"], + textEnd: llmEventTagged.guards["text-end"], + reasoningDelta: llmEventTagged.guards["reasoning-delta"], + toolInputDelta: llmEventTagged.guards["tool-input-delta"], + toolCall: llmEventTagged.guards["tool-call"], + toolResult: llmEventTagged.guards["tool-result"], + toolError: llmEventTagged.guards["tool-error"], + stepFinish: llmEventTagged.guards["step-finish"], + requestFinish: llmEventTagged.guards["request-finish"], + providerError: llmEventTagged.guards["provider-error"], +} as const + +export const LLMEvent = Object.assign(llmEventTagged, { is: llmEventIs }) +export type LLMEvent = Schema.Schema.Type export class PatchTrace extends Schema.Class("LLM.PatchTrace")({ id: Schema.String, diff --git a/packages/llm/test/provider/openai-chat-tool-loop.recorded.test.ts b/packages/llm/test/provider/openai-chat-tool-loop.recorded.test.ts index 658ebf644c..c78f16e161 100644 --- a/packages/llm/test/provider/openai-chat-tool-loop.recorded.test.ts +++ b/packages/llm/test/provider/openai-chat-tool-loop.recorded.test.ts @@ -44,12 +44,12 @@ describe("OpenAI Chat tool-loop recorded", () => { // Two model rounds: tool-call + tool-result + final answer. Two // `request-finish` events confirm both interactions in the cassette // were dispatched in order. - const finishes = events.filter(LLMEvent.guards["request-finish"]) + const finishes = events.filter(LLMEvent.is.requestFinish) expect(finishes).toHaveLength(2) expect(finishes[0]?.reason).toBe("tool-calls") expect(finishes.at(-1)?.reason).toBe("stop") - const toolResult = events.find(LLMEvent.guards["tool-result"]) + const toolResult = events.find(LLMEvent.is.toolResult) expect(toolResult).toMatchObject({ type: "tool-result", name: "get_weather", diff --git a/packages/llm/test/tool-runtime.test.ts b/packages/llm/test/tool-runtime.test.ts index abd7aafcee..39ee254da3 100644 --- a/packages/llm/test/tool-runtime.test.ts +++ b/packages/llm/test/tool-runtime.test.ts @@ -52,7 +52,7 @@ describe("ToolRuntime", () => { ), ) - const result = events.find(LLMEvent.guards["tool-result"]) + const result = events.find(LLMEvent.is.toolResult) expect(result).toMatchObject({ type: "tool-result", id: "call_1", @@ -79,10 +79,10 @@ describe("ToolRuntime", () => { ), ) - const toolError = events.find(LLMEvent.guards["tool-error"]) + const toolError = events.find(LLMEvent.is.toolError) expect(toolError).toMatchObject({ type: "tool-error", id: "call_1", name: "missing_tool" }) expect(toolError?.message).toContain("Unknown tool") - expect(events.find(LLMEvent.guards["tool-result"])).toMatchObject({ + expect(events.find(LLMEvent.is.toolResult)).toMatchObject({ type: "tool-result", id: "call_1", name: "missing_tool", @@ -106,7 +106,7 @@ describe("ToolRuntime", () => { ), ) - const toolError = events.find(LLMEvent.guards["tool-error"]) + const toolError = events.find(LLMEvent.is.toolError) expect(toolError).toMatchObject({ type: "tool-error", id: "call_1", name: "get_weather" }) expect(toolError?.message).toContain("Invalid tool input") }), @@ -127,7 +127,7 @@ describe("ToolRuntime", () => { ), ) - const toolError = events.find(LLMEvent.guards["tool-error"]) + const toolError = events.find(LLMEvent.is.toolError) expect(toolError).toMatchObject({ type: "tool-error", id: "call_1", name: "get_weather" }) expect(toolError?.message).toBe("Weather lookup failed for FAIL") }), @@ -166,7 +166,7 @@ describe("ToolRuntime", () => { ), ) - expect(events.filter(LLMEvent.guards["request-finish"])).toHaveLength(2) + expect(events.filter(LLMEvent.is.requestFinish)).toHaveLength(2) }), ) @@ -186,8 +186,8 @@ describe("ToolRuntime", () => { }).pipe(Stream.runCollect, Effect.provide(layer)), ) - expect(events.filter(LLMEvent.guards["request-finish"])).toHaveLength(1) - expect(events.find(LLMEvent.guards["tool-result"])).toBeUndefined() + expect(events.filter(LLMEvent.is.requestFinish)).toHaveLength(1) + expect(events.find(LLMEvent.is.toolResult)).toBeUndefined() }), ) @@ -238,8 +238,8 @@ describe("ToolRuntime", () => { ) expect(streams).toBe(1) - expect(events.find(LLMEvent.guards["tool-error"])).toBeUndefined() - expect(events.filter(LLMEvent.guards["tool-call"])).toEqual([ + expect(events.find(LLMEvent.is.toolError)).toBeUndefined() + expect(events.filter(LLMEvent.is.toolCall)).toEqual([ { type: "tool-call", id: "srvtoolu_abc", @@ -276,7 +276,7 @@ describe("ToolRuntime", () => { ), ) - const results = events.filter(LLMEvent.guards["tool-result"]) + const results = events.filter(LLMEvent.is.toolResult) expect(results).toHaveLength(2) expect(results.map((event) => event.id).toSorted()).toEqual(["c1", "c2"]) }),