|
|
@@ -112,17 +112,29 @@ export const stream = <T extends Tools>(options: StreamOptions<T>): Stream.Strea
|
|
|
|
|
|
const dispatched = yield* Effect.forEach(
|
|
|
state.toolCalls,
|
|
|
- (call) => dispatch(tools, call).pipe(Effect.map((result) => [call, result] as const)),
|
|
|
+ (call) =>
|
|
|
+ dispatch(tools, call).pipe(Effect.map((result) => [call, result.result, result.error] as const)),
|
|
|
{ concurrency },
|
|
|
)
|
|
|
- const resultStream = Stream.fromIterable(dispatched.flatMap(([call, result]) => emitEvents(call, result)))
|
|
|
+ const resultStream = Stream.fromIterable(
|
|
|
+ dispatched.flatMap(([call, result, error]) => emitEvents(call, result, error)),
|
|
|
+ )
|
|
|
|
|
|
if (!options.stopWhen) return resultStream.pipe(Stream.concat(finishStream))
|
|
|
if (options.stopWhen({ step, request })) return resultStream.pipe(Stream.concat(finishStream))
|
|
|
|
|
|
return resultStream.pipe(
|
|
|
Stream.concat(
|
|
|
- loop(followUpRequest(request, state, dispatched), step + 1, totalUsage, totalProviderMetadata),
|
|
|
+ loop(
|
|
|
+ followUpRequest(
|
|
|
+ request,
|
|
|
+ state,
|
|
|
+ dispatched.map(([call, result]) => [call, result] as const),
|
|
|
+ ),
|
|
|
+ step + 1,
|
|
|
+ totalUsage,
|
|
|
+ totalProviderMetadata,
|
|
|
+ ),
|
|
|
),
|
|
|
)
|
|
|
}),
|
|
|
@@ -215,7 +227,7 @@ const addUsage = (left: Usage | undefined, right: Usage | undefined) => {
|
|
|
| "reasoningTokens"
|
|
|
| "totalTokens"
|
|
|
const sum = (key: UsageKey) =>
|
|
|
- left[key] === undefined && right[key] === undefined ? undefined : Number(left[key] ?? 0) + Number(right[key] ?? 0)
|
|
|
+ left[key] === undefined && right[key] === undefined ? undefined : (left[key] ?? 0) + (right[key] ?? 0)
|
|
|
|
|
|
return new Usage({
|
|
|
inputTokens: sum("inputTokens"),
|
|
|
@@ -264,16 +276,20 @@ const appendStreamingText = (
|
|
|
state.assistantContent.push({ type, text, providerMetadata })
|
|
|
}
|
|
|
|
|
|
-const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<ToolResultValue> => {
|
|
|
+const dispatch = (tools: Tools, call: ToolCallPart): Effect.Effect<{ result: ToolResultValue; error?: unknown }> => {
|
|
|
const tool = tools[call.name]
|
|
|
- if (!tool) return Effect.succeed({ type: "error" as const, value: `Unknown tool: ${call.name}` })
|
|
|
+ if (!tool) return Effect.succeed({ result: { type: "error" as const, value: `Unknown tool: ${call.name}` } })
|
|
|
if (!tool.execute)
|
|
|
- return Effect.succeed({ type: "error" as const, value: `Tool has no execute handler: ${call.name}` })
|
|
|
+ return Effect.succeed({ result: { type: "error" as const, value: `Tool has no execute handler: ${call.name}` } })
|
|
|
|
|
|
return decodeAndExecute(tool, call).pipe(
|
|
|
Effect.catchTag("LLM.ToolFailure", (failure) =>
|
|
|
- Effect.succeed({ type: "error" as const, value: failure.message } satisfies ToolResultValue),
|
|
|
+ Effect.succeed({
|
|
|
+ result: { type: "error" as const, value: failure.message } satisfies ToolResultValue,
|
|
|
+ error: failure.error,
|
|
|
+ }),
|
|
|
),
|
|
|
+ Effect.map((result) => ("result" in result ? result : { result })),
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -294,10 +310,10 @@ const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<Tool
|
|
|
Effect.map((encoded): ToolResultValue => ({ type: "json", value: encoded })),
|
|
|
)
|
|
|
|
|
|
-const emitEvents = (call: ToolCallPart, result: ToolResultValue): ReadonlyArray<LLMEvent> =>
|
|
|
+const emitEvents = (call: ToolCallPart, result: ToolResultValue, error: unknown): ReadonlyArray<LLMEvent> =>
|
|
|
result.type === "error"
|
|
|
? [
|
|
|
- LLMEvent.toolError({ id: call.id, name: call.name, message: String(result.value) }),
|
|
|
+ LLMEvent.toolError({ id: call.id, name: call.name, message: String(result.value), error }),
|
|
|
LLMEvent.toolResult({ id: call.id, name: call.name, result }),
|
|
|
]
|
|
|
: [LLMEvent.toolResult({ id: call.id, name: call.name, result })]
|