feat(core-agent): bump xsai / remove patches (#2164)

## Summary

- Bump catalog `@xsai/*`, `@xsai-ext/providers`, and `xsschema` to
**0.5.0-beta.8**, and delete the three `@xsai/*` pnpm patches — the
beta.2 ones from #1602 and the beta.8 regenerations that landed on main
in `38a008500`.
- Always capture tool failures on the core-agent chat path: xsAI beta.8
marks failed tool executions with `isError: true` on `tool-result.done`,
and `llm-service.ts` maps that to AIRI's `tool-error` event via
`toAiriStreamEvent`, so the agent loop continues instead of aborting.
- Remove the `captureToolErrors` request flag (and stop forwarding it
into `streamText`).
- Rebased onto latest `main` (`b230e16b2`). Includes one follow-up fix:
steps are marked settled before the finish listener runs, and finish
listener failures still reject the stream.

### Related

- Supersedes / follows up on
[#1602](https://github.com/moeru-ai/airi/pull/1602) (`captureToolErrors`
+ xsai patches).

### Scope of capture

| Case | Covered |
| --- | --- |
| A — unknown tool | yes |
| B — invalid / unparseable arguments JSON | yes |
| C — `validate` failure | yes |
| D — `execute` throw | yes |
| `missing_name` / `missing_arguments` | no (xsai still aborts) |
| `repairToolCall` | no |

Error copy on beta.8: `Tool "<toolName>" execution failed: …` (produced
by xsAI).

## Test plan

### Automated (Vitest)

- [x] core-agent `llm-service.test.ts` — 16/16
- [x] core-agent full suite — 82/82
- [x] stage-ui `llm.test.ts` + `chat.contract.test.ts` — 44/44 (the
previous `stepsSettled` timing failure is fixed in this branch)
- [x] stage-ui full suite — 594 passed / 0 failed (one
browser-test-runner teardown error, not a test failure)
- [x] typecheck — core-agent, stage-ui, component-calling, satori-bot
pass; telegram-bot fails only at `src/utils/velin.ts`, which is
pre-existing on main and untouched by this PR

### Real-environment E2E (rebase branch, DeepSeek V4 Flash via DeepSeek
API)

Harness: `/Users/lulu/GitHub/airi-e2e/pr2164/tool-error-e2e-rebase.mjs`
— drives the built `core-agent` `streamFrom` with a deliberately failing
tool.

| Case | Result | Evidence |
| --- | --- | --- |
| D — execute throw | **pass** | `tool-error` carried `Tool
"always_fail" execution failed: boom: deterministic tool failure`; the
model answered: "The always_fail tool threw a deterministic error as
expected." |
| A — unknown tool | **pass** | The model called the unavailable
`search_the_moon_database` after being told truthfully that this tests
AIRI's error capture; runtime returned `tool-error` and the conversation
continued. |
| B — bad arguments JSON | not observed on real model | providers rarely
emit invalid `arguments`; covered by unit test |
| C — `validate` failure | pass (earlier manual run with a temporary
validate-gated tool) | — |

Evidence artifacts:
`/Users/lulu/GitHub/airi-e2e/artifacts/pr2164/tool-error-e2e-2026-08-10T16-49-15-384Z.{json,log}`

### Notes / non-goals

- Fallout-only updates for the xsai beta.8 API rename: `textStream`,
`inputTokens` / `outputTokens` / `totalTokens` in component-calling /
telegram / satori.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Lulu
2026-08-11 17:51:15 +08:00
committed by GitHub
co-authored by Cursor autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
parent b230e16b2e
commit fd8b7a0ae3
18 changed files with 219 additions and 1034 deletions
+10 -12
View File
@@ -365,23 +365,21 @@ async function handleChatSendMessage() {
waiting.value = false
for await (const chunk of response.fullStream) {
if (chunk.type === 'text-delta') {
streamingMessage.value.content += chunk.text
for await (const text of response.textStream) {
streamingMessage.value.content += text
try {
if (chunk.text.length > 1) {
for (const char of chunk.text) {
parser.consume(char)
}
}
else {
parser.consume(chunk.text)
try {
if (text.length > 1) {
for (const char of text) {
parser.consume(char)
}
}
catch {
else {
parser.consume(text)
}
}
catch {
}
}
}
catch (err) {
@@ -1,18 +1,16 @@
import type { StreamTextEvent } from '@xsai/stream-text'
export function mockStreamText(): {
fullStream: ReadableStream<StreamTextEvent>
textStream: ReadableStream<string>
} {
const source = `<component_call><component_name>weather</component_name> \`\`\`json <component_props>{"city":"Shanghai","temperature":"29","condition":"cloudy"}</component_props> \`\`\`</component_call>`
return {
fullStream: new ReadableStream<StreamTextEvent>({
textStream: new ReadableStream<string>({
start(controller) {
const text = source.split('')
let index = 0
const interval = setInterval(() => {
if (index < text.length) {
controller.enqueue({ type: 'text-delta', text: text[index] })
controller.enqueue(text[index]!)
index++
}
else {
@@ -79,9 +79,9 @@ export async function imagineAnAction(
response: res.text,
unreadEvents: Object.fromEntries(Object.entries(globalStates.unreadEvents).map(([key, value]) => [key, value.length])),
now: new Date().toLocaleString(),
totalTokens: res.usage.total_tokens,
promptTokens: res.usage.prompt_tokens,
completion_tokens: res.usage.completion_tokens,
totalTokens: res.usage.totalTokens,
promptTokens: res.usage.inputTokens,
completion_tokens: res.usage.outputTokens,
}).log('Generated action')
responseText = res.text
@@ -27,7 +27,7 @@ export function parseMayStructuredMessage(responseText: string) {
const parsedResponse = parse(result[0]) as ({ messages?: unknown, reply_to_message_id?: unknown } | undefined)
const hasMessagesArray = Array.isArray(parsedResponse?.messages)
const messages = hasMessagesArray
const messages = Array.isArray(parsedResponse?.messages)
? parsedResponse.messages.filter((message): message is string => typeof message === 'string' && message.trim() !== '')
: []
const replyToMessageId = typeof parsedResponse?.reply_to_message_id === 'string'
@@ -109,9 +109,9 @@ export async function sendMessage(
messages: responseText,
response: res.text,
now: new Date().toLocaleString(),
totalTokens: res.usage.total_tokens,
promptTokens: res.usage.prompt_tokens,
completion_tokens: res.usage.completion_tokens,
totalTokens: res.usage.totalTokens,
promptTokens: res.usage.inputTokens,
completion_tokens: res.usage.outputTokens,
}).log('Message split')
const structuredMessage = parseMayStructuredMessage(res.text)
+3 -3
View File
@@ -99,9 +99,9 @@ export async function imagineAnAction(
response: res.text,
unreadMessages: Object.fromEntries(Object.entries(globalStates.unreadMessages).map(([key, value]) => [key, value.length])),
now: new Date().toLocaleString(),
totalTokens: res.usage.total_tokens,
promptTokens: res.usage.prompt_tokens,
completion_tokens: res.usage.completion_tokens,
totalTokens: res.usage.totalTokens,
promptTokens: res.usage.inputTokens,
completion_tokens: res.usage.outputTokens,
}).log('Generated action')
const action = tracer.startActiveSpan('telegram.module.generate_agent_action.parse', (s) => {
@@ -245,8 +245,8 @@ export function createSparkNotifyAgent(options: CreateSparkNotifyAgentOptions):
return
}
if (streamEvent.type === 'tool-result') {
await emit({ type: 'tool-execution', payload: { eventId: request.event.data.eventId, toolCallId: streamEvent.toolCallId, output: streamEvent.result } })
if (streamEvent.type === 'tool-result' || streamEvent.type === 'tool-error') {
await emit({ type: 'tool-execution', payload: { eventId: request.event.data.eventId, kind: streamEvent.type, toolCallId: streamEvent.toolCallId, output: streamEvent.result } })
return
}
@@ -726,7 +726,6 @@ export function createChatOrchestratorRuntime(deps: ChatOrchestratorRuntimeDeps)
},
tools: options.tools,
waitForTools: true,
captureToolErrors: true,
onUsage: (usage) => {
generationUsage = usage
deps.onLlmGeneration?.({
@@ -1,7 +1,7 @@
import type { ChatProvider } from '@xsai-ext/providers/utils'
import type { Message, Tool } from '@xsai/shared-chat'
import { describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { isContentArrayRelatedError, sanitizeMessages, streamFrom } from './llm-service'
@@ -39,7 +39,10 @@ function createMockStreamResult(
}
}
describe('streamFrom tool error capture', () => {
describe('streamFrom tool errors', () => {
beforeEach(() => {
streamTextMock.mockReset()
})
it('requests final streaming usage and emits the reported token totals once', async () => {
const onUsage = vi.fn()
streamTextMock.mockReturnValueOnce(createMockStreamResult(
@@ -140,11 +143,7 @@ describe('streamFrom tool error capture', () => {
})).resolves.toBeUndefined()
})
/**
* @example
* await streamFrom({ model, chatProvider, messages, options: { captureToolErrors: true } })
*/
it('keeps captureToolErrors internal while forwarding failed tool calls as tool-error events', async () => {
it('maps xsai tool-error results to AIRI tool-error events without wrapping tools', async () => {
let resolveSteps: ((steps: unknown[]) => void) | undefined
const events: unknown[] = []
const failingTool = {
@@ -160,8 +159,8 @@ describe('streamFrom tool error capture', () => {
} satisfies Tool
streamTextMock.mockImplementationOnce((options: {
captureToolErrors?: boolean
onEvent: (event: unknown) => Promise<void>
preToolCall?: unknown
tools?: Tool[]
}) => {
const steps = new Promise<unknown[]>((resolve) => {
@@ -169,19 +168,15 @@ describe('streamFrom tool error capture', () => {
})
queueMicrotask(async () => {
const result = await options.tools?.[0]?.execute({}, {
messages: [],
toolCallId: 'call-1',
})
await options.onEvent({
type: 'tool-result',
type: 'tool-result.done',
args: {},
result,
isError: true,
result: 'Tool "play_chess" execution failed: Focus mode does not accept game-state mutation inputs.',
toolCallId: 'call-1',
toolName: 'play_chess',
})
await options.onEvent({ type: 'finish', finishReason: 'stop' })
await options.onEvent({ type: 'text.delta', delta: 'ok' })
resolveSteps?.([])
})
@@ -193,7 +188,6 @@ describe('streamFrom tool error capture', () => {
chatProvider: provider,
messages: [{ role: 'user', content: 'play chess' }] as Message[],
options: {
captureToolErrors: true,
tools: [failingTool],
onStreamEvent: (event) => {
events.push(event)
@@ -202,16 +196,35 @@ describe('streamFrom tool error capture', () => {
})
const streamOptions = streamTextMock.mock.calls[0]?.[0]
expect(streamOptions.captureToolErrors).toBeUndefined()
expect(streamOptions.tools?.[0]).not.toBe(failingTool)
expect(failingTool.execute).toHaveBeenCalledTimes(1)
expect(events).toContainEqual(expect.objectContaining({
expect(streamOptions.preToolCall).toBeUndefined()
expect(streamOptions.tools?.[0]).toBe(failingTool)
expect(failingTool.execute).not.toHaveBeenCalled()
expect(events).toContainEqual({
type: 'tool-error',
args: {},
isError: true,
result: 'Tool "play_chess" execution failed: Focus mode does not accept game-state mutation inputs.',
toolCallId: 'call-1',
toolName: 'play_chess',
result: expect.stringContaining('Focus mode does not accept game-state mutation inputs.'),
}))
})
expect(events).toContainEqual({ type: 'text-delta', text: 'ok' })
expect(events).toContainEqual({ type: 'finish' })
})
it('rejects when the finish listener throws instead of leaving the stream pending', async () => {
streamTextMock.mockReturnValueOnce(createMockStreamResult())
await expect(streamFrom({
model: 'model-a',
chatProvider: provider,
messages: [{ role: 'user', content: 'hello' }] as Message[],
options: {
onStreamEvent: async (event) => {
if (event.type === 'finish')
throw new Error('finish listener failed')
},
},
})).rejects.toThrow('finish listener failed')
})
})
+64 -88
View File
@@ -1,13 +1,11 @@
import type { ChatProvider } from '@xsai-ext/providers/utils'
import type { Message, Tool, Usage } from '@xsai/shared-chat'
import type { Event, Message, Usage } from '@xsai/shared-chat'
import type { StreamFromOptions, StreamOptions } from '../types/llm'
import type { StreamEvent, StreamFromOptions, StreamOptions } from '../types/llm'
import { stepCountAtLeast } from '@xsai/shared-chat'
import { streamText } from '@xsai/stream-text'
import { errorMessageFromValue } from '../utils/error-message'
/**
* Normalize chat messages so they match the wire format the active provider
* actually accepts, flattening content-part arrays back to plain strings when
@@ -103,75 +101,45 @@ async function resolveTools(options?: StreamOptions) {
return tools ?? []
}
function isAbortError(error: unknown): boolean {
return typeof error === 'object'
&& error !== null
&& (error as { name?: unknown }).name === 'AbortError'
}
function createCapturedToolErrorResult(toolName: string, error: unknown): string {
return `Tool call error for "${toolName}": ${errorMessageFromValue(error)}`
}
function normalizeUsage(usage: Usage | undefined) {
if (usage?.inputTokens == null || usage.outputTokens == null || usage.totalTokens == null) {
return { source: 'unavailable' as const }
}
return {
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
totalTokens: usage.totalTokens,
source: 'reported' as const,
}
}
function withCapturedToolErrors(
tools: Tool[],
capturedToolErrorByCallId: Map<string, string>,
): Tool[] {
return tools.map(tool => ({
...tool,
execute: async (input, executeOptions) => {
try {
return await tool.execute(input, executeOptions)
/**
* Maps xsAI stream events onto the AIRI {@link StreamEvent} contract.
*
* xsAI 0.5.0-beta.8 marks failed tool executions with `isError: true` on
* `tool-result.done` instead of aborting the stream, so AIRI can distinguish
* `tool-error` from `tool-result` directly from the event payload.
*/
function toAiriStreamEvent(event: Event): StreamEvent | null {
switch (event.type) {
case 'text.delta':
return { type: 'text-delta', text: event.delta }
case 'reasoning.delta':
return { type: 'reasoning-delta', text: event.delta }
case 'tool-call.done':
return { ...event, type: 'tool-call' }
case 'tool-result.done':
if (event.isError === true)
return { ...event, type: 'tool-error', isError: true }
return {
type: 'tool-result',
toolCallId: event.toolCallId,
result: typeof event.result === 'string' || Array.isArray(event.result)
? event.result
: JSON.stringify(event.result),
}
catch (error) {
if (isAbortError(error))
throw error
const result = createCapturedToolErrorResult(tool.function.name, error)
capturedToolErrorByCallId.set(executeOptions.toolCallId, result)
return result
case 'error':
return {
type: 'error',
error: event.cause ?? new Error(event.message),
}
},
}))
}
function resolveCapturedToolErrorEvent(
event: unknown,
capturedToolErrorByCallId: Map<string, string>,
) {
if (
typeof event !== 'object'
|| event === null
|| (event as { type?: unknown }).type !== 'tool-result'
|| typeof (event as { toolCallId?: unknown }).toolCallId !== 'string'
) {
return event
}
const toolCallId = (event as { toolCallId: string }).toolCallId
const result = capturedToolErrorByCallId.get(toolCallId)
if (result == null)
return event
capturedToolErrorByCallId.delete(toolCallId)
return {
...event,
type: 'tool-error',
isError: true,
result,
case 'text.start':
case 'text.done':
case 'reasoning.start':
case 'reasoning.done':
case 'step.start':
case 'step.done':
case 'tool-call.start':
case 'tool-call.delta':
return null
}
}
@@ -193,10 +161,6 @@ export async function streamFrom({
const customTools = supportedTools ? await resolveTools(options) : []
const mergedTools = supportedTools ? [...builtinTools, ...customTools] : []
const tools = mergedTools.length > 0 ? mergedTools : undefined
const capturedToolErrorByCallId = new Map<string, string>()
const streamTools = options?.captureToolErrors && tools != null
? withCapturedToolErrors(tools, capturedToolErrorByCallId)
: tools
return new Promise<void>((resolve, reject) => {
let settled = false
@@ -214,13 +178,13 @@ export async function streamFrom({
reject(error)
}
const onEvent = async (event: unknown) => {
const onEvent = async (event: Event) => {
try {
const streamEvent = resolveCapturedToolErrorEvent(event, capturedToolErrorByCallId)
await options?.onStreamEvent?.(streamEvent as any)
if (event && (event as any).type === 'error') {
rejectOnce((event as any).error ?? new Error('Stream error'))
}
const streamEvent = toAiriStreamEvent(event)
if (streamEvent != null)
await options?.onStreamEvent?.(streamEvent)
if (streamEvent?.type === 'error')
rejectOnce(streamEvent.error)
}
catch (error) {
rejectOnce(error)
@@ -235,12 +199,7 @@ export async function streamFrom({
headers: options?.headers,
streamOptions: { includeUsage: true },
stopWhen: stepCountAtLeast(10),
// NOTICE:
// Do not pass xsAI's `captureToolErrors` option here. In the installed
// @xsai/stream-text version, stream options are spread into the provider
// chat body, so unknown runtime-only fields can be rejected upstream.
// AIRI captures tool failures by wrapping local tool executors instead.
tools: streamTools,
tools,
toolChoice: options?.toolChoice,
onEvent,
})
@@ -263,6 +222,19 @@ export async function streamFrom({
// Ignore any late provider error event emitted after xsAI has already
// resolved the authoritative full-step lifecycle.
stepsSettled = true
try {
await options?.onStreamEvent?.({ type: 'finish' } as const)
}
catch (error) {
// The finish listener runs after steps settled, so rejectOnce would
// ignore this error as a "late provider event". A listener failure
// is still a real failure and must reject the outer promise.
if (!settled) {
settled = true
reject(error)
}
return
}
let usage: Usage | undefined
try {
usage = await streamResult.totalUsage
@@ -271,7 +243,11 @@ export async function streamFrom({
console.error('Stream totalUsage error:', error)
}
try {
await options?.onUsage?.(normalizeUsage(usage))
const normalizedUsage = !usage
|| (usage.inputTokens == null && usage.outputTokens == null && usage.totalTokens == null)
? { source: 'unavailable' as const }
: { ...usage, source: 'reported' as const }
await options?.onUsage?.(normalizedUsage)
}
catch (error) {
// Usage observers are telemetry-only and must not turn a completed
+1 -2
View File
@@ -17,7 +17,7 @@ export type StreamEvent
| { type: 'reasoning-delta', text: string }
| ({ type: 'finish' } & any)
| ({ type: 'tool-call' } & CompletionToolCall)
| (CompletionToolResult & { type: 'tool-error' })
| (CompletionToolResult & { type: 'tool-error', isError: true })
| { type: 'tool-result', toolCallId: string, result?: string | CommonContentPart[] }
| { type: 'error', error: any }
@@ -35,7 +35,6 @@ export interface StreamOptions {
toolsCompatibility?: Map<string, boolean>
supportsTools?: boolean
waitForTools?: boolean
captureToolErrors?: boolean
/** Provider tool-selection directive for one request. */
toolChoice?: ToolChoice
tools?: Tool[] | (() => Promise<Tool[] | undefined>)
@@ -129,40 +129,32 @@ describe('isToolRelatedError', () => {
})
}
it('resolves from steps while still forwarding tool_calls finish events', async () => {
let onEvent: ((event: unknown) => Promise<void>) | undefined
streamTextMock.mockImplementation((options: { onEvent: (event: unknown) => Promise<void> }) => {
onEvent = options.onEvent
return createMockStreamResult()
})
it('resolves from steps and emits a single finish event', async () => {
streamTextMock.mockImplementation(() => createMockStreamResult())
const store = useLLM()
const onStreamEvent = vi.fn()
let resolved = false
const pending = store.stream('model-a', provider, [{ role: 'user', content: 'hello' }] as Message[], {
await store.stream('model-a', provider, [{ role: 'user', content: 'hello' }] as Message[], {
waitForTools: true,
onStreamEvent,
}).then(() => {
resolved = true
})
await vi.waitFor(() => expect(onEvent).toBeTypeOf('function'))
await onEvent!({ type: 'finish', finishReason: 'tool_calls' })
await Promise.resolve()
expect(resolved).toBe(true)
await onEvent!({ type: 'finish', finishReason: 'stop' })
await pending
expect(onStreamEvent).toHaveBeenCalledTimes(2)
expect(onStreamEvent).toHaveBeenCalledTimes(1)
expect(onStreamEvent).toHaveBeenCalledWith({ type: 'finish' })
})
it('ignores later error events after steps have resolved', async () => {
let onEvent: ((event: unknown) => Promise<void>) | undefined
let resolveSteps: ((steps: unknown[]) => void) | undefined
streamTextMock.mockImplementation((options: { onEvent: (event: unknown) => Promise<void> }) => {
onEvent = options.onEvent
return createMockStreamResult()
return {
...createMockStreamResult(),
steps: new Promise<unknown[]>((resolve) => {
resolveSteps = resolve
}),
}
})
const store = useLLM()
@@ -171,18 +163,27 @@ describe('isToolRelatedError', () => {
})
await vi.waitFor(() => expect(onEvent).toBeTypeOf('function'))
await onEvent!({ type: 'finish', finishReason: 'tool_calls' })
await onEvent!({ type: 'error', error: new Error('stream failed') })
resolveSteps?.([])
await Promise.resolve()
await onEvent!({ type: 'error', message: 'stream failed', cause: new Error('stream failed') })
await expect(pending).resolves.toBeUndefined()
})
it('keeps builtin tools when stream steps resolve before a tool-related error event', async () => {
const store = useLLM()
const llmToolsStore = useLlmToolsStore()
const customTool = { name: 'custom-tool' } as any
const customTool = {
type: 'function',
function: {
name: 'custom-tool',
description: 'Custom tool.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(async () => 'ok'),
} satisfies Tool
const runtimeTool = {
id: 'plugin:chess:runtime_play_chess_match',
type: 'function',
type: 'function' as const,
function: {
name: 'runtime_play_chess_match',
description: 'Start a runtime chess match.',
@@ -195,7 +196,7 @@ describe('isToolRelatedError', () => {
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'error', error: new Error('model does not support tools') })
await options.onEvent({ type: 'error', message: 'model does not support tools', cause: new Error('model does not support tools') })
})
return createMockStreamResult()
})
@@ -208,15 +209,10 @@ describe('isToolRelatedError', () => {
expect(Array.isArray(firstCallTools)).toBe(true)
expect(mcpMock).toHaveBeenCalledTimes(1)
expect(debugMock).toHaveBeenCalledTimes(1)
expect(firstCallTools).toContain(customTool)
expect(firstCallTools?.map(toolNameFrom)).toContain('custom-tool')
expect(firstCallTools?.map(toolNameFrom)).toContain('runtime_play_chess_match')
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'finish', finishReason: 'stop' })
})
return createMockStreamResult()
})
streamTextMock.mockImplementationOnce(() => createMockStreamResult())
await store.stream('model-a', provider, [{ role: 'user', content: 'hello again' }] as Message[], {
tools: [customTool],
@@ -232,7 +228,7 @@ describe('isToolRelatedError', () => {
const llmToolsStore = useLlmToolsStore()
const playChessTool = {
id: 'plugin:chess:runtime_open_chess_board',
type: 'function',
type: 'function' as const,
function: {
name: 'runtime_open_chess_board',
description: 'Open the runtime chess board.',
@@ -242,7 +238,7 @@ describe('isToolRelatedError', () => {
} satisfies ExecutableTool
const runtimeMcpStatusTool = {
id: 'mcp:runtime_sync_mcp_status',
type: 'function',
type: 'function' as const,
function: {
name: 'runtime_sync_mcp_status',
description: 'Sync runtime MCP status.',
@@ -253,12 +249,7 @@ describe('isToolRelatedError', () => {
llmToolsStore.addTools(runtimeMcpStatusTool, playChessTool)
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'finish', finishReason: 'stop' })
})
return createMockStreamResult()
})
streamTextMock.mockImplementationOnce(() => createMockStreamResult())
await store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
@@ -273,6 +264,7 @@ describe('isToolRelatedError', () => {
const store = useLLM()
const llmToolsStore = useLlmToolsStore()
const builtinTool = {
type: 'function',
function: {
name: 'duplicate_runtime_tool',
description: 'Builtin version.',
@@ -282,7 +274,7 @@ describe('isToolRelatedError', () => {
} as unknown as Tool
const runtimeTool = {
id: 'plugin:runtime:duplicate_runtime_tool',
type: 'function',
type: 'function' as const,
function: {
name: 'duplicate_runtime_tool',
description: 'Runtime version.',
@@ -294,16 +286,11 @@ describe('isToolRelatedError', () => {
mcpMock.mockResolvedValueOnce([builtinTool] as Tool[])
llmToolsStore.addTools(runtimeTool)
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'finish', finishReason: 'stop' })
})
return createMockStreamResult()
})
streamTextMock.mockImplementationOnce(() => createMockStreamResult())
await store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools as Array<{ function?: { name?: string } }>
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools as Array<{ function?: { name?: string, description?: string } }>
const duplicateNameTools = mergedTools.filter(tool => tool.function?.name === 'duplicate_runtime_tool')
expect(duplicateNameTools).toHaveLength(1)
@@ -518,7 +518,7 @@ describe('chat store contract', () => {
llmStreamMock.mockImplementation(async (_model: string, _chatProvider: ChatProvider, messages: Message[], options: any) => {
composedMessages = messages
expect(options.waitForTools).toBe(true)
expect(options.captureToolErrors).toBe(true)
expect(options.captureToolErrors).toBeUndefined()
await options.onStreamEvent({ type: 'text-delta', text: 'hello' })
await options.onStreamEvent({ type: 'finish', finishReason: 'stop' })
@@ -1,294 +0,0 @@
import type { Message, Tool, ToolCall } from '@xsai/shared-chat'
import { InvalidToolCallError, InvalidToolInputError, ToolExecutionError } from '@xsai/shared'
import { executeTool } from '@xsai/shared-chat'
import { describe, expect, it, vi } from 'vitest'
function createToolCall(overrides: Partial<ToolCall> & {
function?: Partial<ToolCall['function']> & { name?: string, arguments?: string }
} = {}): ToolCall {
const fn = overrides.function ?? {}
return {
id: 'call_1',
type: 'function',
function: {
name: 'myTool',
arguments: '{}',
...fn,
},
...overrides,
} as ToolCall
}
function createTool(name: string, execute: Tool['execute']): Tool {
return {
type: 'function',
function: { name, description: '', parameters: {} },
execute,
}
}
const emptyMessages: Message[] = []
describe('executeTool (patched @xsai/shared-chat)', () => {
it('returns success tool message when tool executes', async () => {
const tools = [createTool('myTool', async () => 'ok')]
const toolCall = createToolCall()
const out = await executeTool({
messages: emptyMessages,
toolCall,
tools,
})
expect(out.completionToolResult.isError).toBeUndefined()
expect(out.completionToolResult.result).toBe('ok')
expect(out.message.role).toBe('tool')
expect(out.message.content).toBe('ok')
expect(out.message.tool_call_id).toBe('call_1')
})
it('captures unknown tool as error result instead of throwing', async () => {
const tools = [createTool('other', async () => 'x')]
const toolCall = createToolCall({ function: { name: 'missingTool', arguments: '{}' } })
const out = await executeTool({
captureToolErrors: true,
messages: emptyMessages,
toolCall,
tools,
})
expect(out.completionToolResult.isError).toBe(true)
expect(out.completionToolResult.error).toBeDefined()
expect(InvalidToolCallError.isInstance(out.completionToolResult.error)).toBe(true)
expect(String(out.message.content)).toContain('missingTool')
expect(out.message.role).toBe('tool')
})
it('captures invalid JSON arguments as error result', async () => {
const tools = [createTool('myTool', async () => 'x')]
const toolCall = createToolCall({ function: { name: 'myTool', arguments: '{broken' } })
const out = await executeTool({
captureToolErrors: true,
messages: emptyMessages,
toolCall,
tools,
})
expect(out.completionToolResult.isError).toBe(true)
expect(InvalidToolInputError.isInstance(out.completionToolResult.error)).toBe(true)
expect(String(out.message.content)).toContain('myTool')
})
it('captures tool execute rejection as error result', async () => {
const tools = [
createTool('myTool', async () => {
throw new Error('execute failed')
}),
]
const toolCall = createToolCall()
const out = await executeTool({
captureToolErrors: true,
messages: emptyMessages,
toolCall,
tools,
})
expect(out.completionToolResult.isError).toBe(true)
expect(ToolExecutionError.isInstance(out.completionToolResult.error)).toBe(true)
expect(String(out.message.content)).toContain('myTool')
expect(String(out.message.content)).toContain('execution failed')
})
it('rethrows AbortError from tool execute', async () => {
const controller = new AbortController()
const tools = [
createTool('myTool', async () => {
const err = new Error('aborted')
err.name = 'AbortError'
throw err
}),
]
const toolCall = createToolCall()
await expect(
executeTool({
abortSignal: controller.signal,
messages: emptyMessages,
toolCall,
tools,
}),
).rejects.toMatchObject({ name: 'AbortError' })
})
it('repairs invalid tool call when repairToolCall returns a valid call', async () => {
const tools = [
createTool('goodTool', async () => 'repaired'),
]
const toolCall = createToolCall({ function: { name: 'badTool', arguments: '{}' } })
const out = await executeTool({
messages: emptyMessages,
repairToolCall: async () => ({
id: 'call_1',
type: 'function',
function: { name: 'goodTool', arguments: '{}' },
}),
toolCall,
tools,
})
expect(out.completionToolResult.isError).toBeUndefined()
expect(out.completionToolResult.result).toBe('repaired')
})
it('returns error when repairToolCall returns null', async () => {
const tools = [createTool('goodTool', async () => 'x')]
const toolCall = createToolCall({ function: { name: 'badTool', arguments: '{}' } })
const out = await executeTool({
captureToolErrors: true,
messages: emptyMessages,
repairToolCall: async () => null,
toolCall,
tools,
})
expect(out.completionToolResult.isError).toBe(true)
expect(InvalidToolCallError.isInstance(out.completionToolResult.error)).toBe(true)
})
it('invokes lifecycle callbacks on success and on error', async () => {
const onToolCallStart = vi.fn()
const onToolCallFinish = vi.fn()
const tools = [createTool('myTool', async () => 'done')]
const successCall = createToolCall()
const successOut = await executeTool({
messages: emptyMessages,
onToolCallFinish,
onToolCallStart,
toolCall: successCall,
tools,
})
expect(onToolCallStart).toHaveBeenCalledOnce()
expect(onToolCallStart).toHaveBeenCalledWith({
input: {},
toolCallId: 'call_1',
toolName: 'myTool',
})
expect(onToolCallFinish).toHaveBeenCalled()
const successFinish = onToolCallFinish.mock.calls[0][0]
expect(successFinish.toolName).toBe('myTool')
expect(successFinish.toolCallId).toBe('call_1')
expect(successFinish.output).toBe(successOut.completionToolResult.result)
expect(successFinish.error).toBeUndefined()
expect(typeof successFinish.durationMs).toBe('number')
onToolCallStart.mockClear()
onToolCallFinish.mockClear()
const badCall = createToolCall({ function: { name: 'nope', arguments: '{}' } })
await executeTool({
captureToolErrors: true,
messages: emptyMessages,
onToolCallFinish,
onToolCallStart,
toolCall: badCall,
tools,
})
expect(onToolCallStart).not.toHaveBeenCalled()
expect(onToolCallFinish).toHaveBeenCalledOnce()
const errFinish = onToolCallFinish.mock.calls[0][0]
expect(errFinish.output).toBeUndefined()
expect(InvalidToolCallError.isInstance(errFinish.error)).toBe(true)
})
describe('without captureToolErrors (default upstream behavior)', () => {
it('throws InvalidToolCallError for unknown tool', async () => {
const tools = [createTool('other', async () => 'x')]
const toolCall = createToolCall({ function: { name: 'missingTool', arguments: '{}' } })
let thrown: unknown
try {
await executeTool({
messages: emptyMessages,
toolCall,
tools,
})
expect.fail('expected executeTool to throw')
}
catch (error) {
thrown = error
}
expect(InvalidToolCallError.isInstance(thrown)).toBe(true)
})
it('throws InvalidToolInputError for invalid JSON arguments', async () => {
const tools = [createTool('myTool', async () => 'x')]
const toolCall = createToolCall({ function: { name: 'myTool', arguments: '{broken' } })
let thrown: unknown
try {
await executeTool({
messages: emptyMessages,
toolCall,
tools,
})
expect.fail('expected executeTool to throw')
}
catch (error) {
thrown = error
}
expect(InvalidToolInputError.isInstance(thrown)).toBe(true)
})
it('throws ToolExecutionError when tool execute rejects', async () => {
const tools = [
createTool('myTool', async () => {
throw new Error('execute failed')
}),
]
const toolCall = createToolCall()
let thrown: unknown
try {
await executeTool({
messages: emptyMessages,
toolCall,
tools,
})
expect.fail('expected executeTool to throw')
}
catch (error) {
thrown = error
}
expect(ToolExecutionError.isInstance(thrown)).toBe(true)
})
it('throws InvalidToolCallError when repairToolCall returns null', async () => {
const tools = [createTool('goodTool', async () => 'x')]
const toolCall = createToolCall({ function: { name: 'badTool', arguments: '{}' } })
let thrown: unknown
try {
await executeTool({
messages: emptyMessages,
repairToolCall: async () => null,
toolCall,
tools,
})
expect.fail('expected executeTool to throw')
}
catch (error) {
thrown = error
}
expect(InvalidToolCallError.isInstance(thrown)).toBe(true)
})
})
})
@@ -1,39 +0,0 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index c9cc51d8b715b6f220d67574b8c674247d032926..9b4c2e6b096a0a139792d3fd4ce89c2f15bfc51e 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,11 +1,15 @@
import { WithUnknown } from '@xsai/shared';
-import { ChatOptions, CompletionStep, PostToolCall, PrepareStep, PreToolCall, StopCondition, Message, Usage, FinishReason, AssistantMessage, ChatCompletionUsage, CompletionToolCall, CompletionToolResult } from '@xsai/shared-chat';
+import { ChatOptions, CompletionStep, OnToolCallFinishCallback, OnToolCallStartCallback, PostToolCall, PrepareStep, PreToolCall, RepairToolCallFunction, StopCondition, Message, Usage, FinishReason, AssistantMessage, ChatCompletionUsage, CompletionToolCall, CompletionToolResult } from '@xsai/shared-chat';
interface GenerateTextOptions extends ChatOptions {
onStepFinish?: (step: CompletionStep<true>) => Promise<unknown> | unknown;
+ captureToolErrors?: boolean;
+ onToolCallFinish?: OnToolCallFinishCallback;
+ onToolCallStart?: OnToolCallStartCallback;
postToolCall?: PostToolCall;
prepareStep?: PrepareStep;
preToolCall?: PreToolCall;
+ repairToolCall?: RepairToolCallFunction;
/** @internal */
steps?: CompletionStep<true>[];
/** @default `stepCountAtLeast(1)` */
diff --git a/dist/index.js b/dist/index.js
index d4fdab8b13f83fc2eb55eca0d15919133347393e..334428415f242fc6b11c874e988bc98187c3eadc 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -55,9 +55,13 @@ const rawGenerateText = async (options) => {
const results = await Promise.all(
msgToolCalls.map(async (toolCall) => executeTool({
abortSignal: options.abortSignal,
+ captureToolErrors: options.captureToolErrors,
messages,
+ onToolCallFinish: options.onToolCallFinish,
+ onToolCallStart: options.onToolCallStart,
postToolCall: options.postToolCall,
preToolCall: options.preToolCall,
+ repairToolCall: options.repairToolCall,
toolCall,
tools: options.tools
}))
@@ -1,211 +0,0 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index b327ec1c9dda3a8c4ddf39beb6a7b4ffc6afb860..8f740ffc45080cf6b80155e62fa87b090aa8efff 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -94,6 +94,7 @@ interface CompletionToolCall {
}
interface CompletionToolResult {
args: unknown;
+ error?: unknown;
isError?: boolean;
result: ToolExecuteResult;
toolCallId: string;
@@ -101,6 +102,24 @@ interface CompletionToolResult {
}
type PostToolCall = (toolResult: CompletionToolResult, options: ToolExecuteOptions) => CompletionToolResult | Promise<CompletionToolResult | void> | void;
type PreToolCall = (toolCall: CompletionToolCall, options: ToolExecuteOptions) => CompletionToolCall | CompletionToolResult | Promise<CompletionToolCall | CompletionToolResult | void> | void;
+type OnToolCallFinishCallback = (context: {
+ durationMs: number;
+ error?: unknown;
+ output?: unknown;
+ toolCallId: string;
+ toolName: string;
+}) => Promise<void> | void;
+type OnToolCallStartCallback = (context: {
+ input: unknown;
+ toolCallId: string;
+ toolName: string;
+}) => Promise<void> | void;
+type RepairToolCallFunction = (context: {
+ error: Error;
+ messages: Message[];
+ toolCall: ToolCall;
+ tools?: Tool[];
+}) => Promise<ToolCall | null> | ToolCall | null;
interface Tool {
execute: (input: unknown, options: ToolExecuteOptions) => Promise<ToolExecuteResult> | ToolExecuteResult;
function: {
@@ -275,9 +294,13 @@ declare const chat: <T extends WithUnknown<ChatOptions>>(options: T) => Promise<
interface ExecuteToolOptions<T = ToolMessage['content']> {
abortSignal?: AbortSignal;
+ captureToolErrors?: boolean;
messages: Message[];
+ onToolCallFinish?: OnToolCallFinishCallback;
+ onToolCallStart?: OnToolCallStartCallback;
postToolCall?: PostToolCall;
preToolCall?: PreToolCall;
+ repairToolCall?: RepairToolCallFunction;
toolCall: ToolCall;
tools?: Tool[];
wrapResult?: (result: ToolExecuteResult) => T;
@@ -285,10 +308,11 @@ interface ExecuteToolOptions<T = ToolMessage['content']> {
interface ExecuteToolResult<T = ToolMessage['content']> {
completionToolCall: CompletionToolCall;
completionToolResult: CompletionToolResult;
+ message: ToolMessage;
result: T;
}
declare const toCompletionToolCall: (toolCall: ToolCall) => CompletionToolCall;
-declare const executeTool: <T = ToolMessage["content"]>({ abortSignal, messages, postToolCall, preToolCall, toolCall, tools, wrapResult }: ExecuteToolOptions<T>) => Promise<ExecuteToolResult<T>>;
+declare const executeTool: <T = ToolMessage["content"]>(options: ExecuteToolOptions<T>) => Promise<ExecuteToolResult<T>>;
interface ResolvePrepareStepOptions<TInput = Message[], TToolChoice = ToolChoice> {
input: TInput;
@@ -317,4 +341,4 @@ declare const computeTotalUsage: (totalUsage: undefined | Usage, usage: Usage) =
declare const normalizeChatCompletionUsage: (usage: ChatCompletionUsage) => Usage;
export { and, chat, computeTotalUsage, executeTool, hasToolCall, normalizeChatCompletionUsage, not, or, resolvePrepareStep, shouldStop, stepCountAtLeast, toCompletionToolCall };
-export type { AssistantMessage, AudioContentPart, ChatCompletionUsage, ChatOptions, CommonContentPart, CompletionStep, CompletionToolCall, CompletionToolResult, DeveloperMessage, ErrorEvent, Event, EventType, ExecuteToolOptions, ExecuteToolResult, FileContentPart, FinishReason, ImageContentPart, Message, PostToolCall, PreToolCall, PrepareStep, PrepareStepOptions, PrepareStepResult, ReasoningDeltaEvent, ReasoningDoneEvent, ReasoningStartEvent, RefusalContentPart, ResolvePrepareStepOptions, ResolvePrepareStepResult, StepDoneEvent, StepStartEvent, StopCondition, StopContext, SystemMessage, TextContentPart, TextDeltaEvent, TextDoneEvent, TextStartEvent, Tool, ToolCall, ToolCallDeltaEvent, ToolCallDoneEvent, ToolCallStartEvent, ToolChoice, ToolExecuteOptions, ToolExecuteResult, ToolMessage, ToolResultDoneEvent, ToolValidateFailure, ToolValidateResult, ToolValidateSuccess, Usage, UserMessage };
+export type { AssistantMessage, AudioContentPart, ChatCompletionUsage, ChatOptions, CommonContentPart, CompletionStep, CompletionToolCall, CompletionToolResult, DeveloperMessage, ErrorEvent, Event, EventType, ExecuteToolOptions, ExecuteToolResult, FileContentPart, FinishReason, ImageContentPart, Message, OnToolCallFinishCallback, OnToolCallStartCallback, PostToolCall, PreToolCall, PrepareStep, PrepareStepOptions, PrepareStepResult, RepairToolCallFunction, ReasoningDeltaEvent, ReasoningDoneEvent, ReasoningStartEvent, RefusalContentPart, ResolvePrepareStepOptions, ResolvePrepareStepResult, StepDoneEvent, StepStartEvent, StopCondition, StopContext, SystemMessage, TextContentPart, TextDeltaEvent, TextDoneEvent, TextStartEvent, Tool, ToolCall, ToolCallDeltaEvent, ToolCallDoneEvent, ToolCallStartEvent, ToolChoice, ToolExecuteOptions, ToolExecuteResult, ToolMessage, ToolResultDoneEvent, ToolValidateFailure, ToolValidateResult, ToolValidateSuccess, Usage, UserMessage };
diff --git a/dist/index.js b/dist/index.js
index d8fa061c8fe9ba9b76aafea74f40e27a957b0a17..45373a31d56a3127377789177e28e609c99fca64 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1,4 +1,4 @@
-import { postJSON, InvalidToolCallError, InvalidToolInputError } from '@xsai/shared';
+import { postJSON, InvalidToolCallError, InvalidToolInputError, ToolExecutionError } from '@xsai/shared';
const chat = async (options) => postJSON("chat/completions", {
...options,
@@ -56,15 +56,19 @@ const parseToolInput = async (tool, input) => {
};
const createErrorToolResult = (toolCall, args, cause, abortSignal) => ({
args,
+ error: cause,
isError: true,
result: `Tool "${toolCall.toolName}" execution failed: ${abortSignal?.aborted === true ? "This operation was aborted" : cause instanceof Error ? cause.message : String(cause)}`,
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName
});
-const catchToolError = async (toolCall, abortSignal, callback) => {
+const isAbortError = (error, abortSignal) => abortSignal?.aborted === true || error instanceof Error && error.name === "AbortError";
+const catchToolError = async (toolCall, abortSignal, captureToolErrors, callback) => {
try {
return await callback(toolCall);
} catch (cause) {
+ if (isAbortError(cause, abortSignal) || !captureToolErrors)
+ throw cause;
return createErrorToolResult(toolCall, InvalidToolInputError.isInstance(cause) ? cause.toolInput : toolCall.args, cause, abortSignal);
}
};
@@ -90,7 +94,7 @@ const findTool = (tools, toolName, toolCall) => {
}
return tool;
};
-const executeTool = async ({ abortSignal, messages, postToolCall, preToolCall, toolCall, tools, wrapResult }) => {
+const executeToolBase = async ({ abortSignal, captureToolErrors, messages, postToolCall, preToolCall, toolCall, tools, wrapResult }) => {
const wrap = wrapResult ?? toToolMessageContent;
const toolName = toolCall.function.name;
const toolArguments = toolCall.function.arguments;
@@ -120,7 +124,7 @@ const executeTool = async ({ abortSignal, messages, postToolCall, preToolCall, t
let completionToolResult;
let parsedArgs;
let shouldPostToolCall = false;
- const preToolCallResult = await catchToolError(completionToolCall, abortSignal, async (toolCall2) => preToolCall?.(toolCall2, toolExecuteOptions));
+ const preToolCallResult = await catchToolError(completionToolCall, abortSignal, captureToolErrors, async (toolCall2) => preToolCall?.(toolCall2, toolExecuteOptions));
if (preToolCallResult) {
assertSameToolCallId(completionToolCall.toolCallId, preToolCallResult, "preToolCallResult");
if ("result" in preToolCallResult)
@@ -128,13 +132,25 @@ const executeTool = async ({ abortSignal, messages, postToolCall, preToolCall, t
else
completionToolCall = preToolCallResult;
}
- completionToolResult ??= await catchToolError(completionToolCall, abortSignal, async () => {
+ completionToolResult ??= await catchToolError(completionToolCall, abortSignal, captureToolErrors, async () => {
const tool = findTool(tools, completionToolCall.toolName, completionToolCall);
parsedArgs = await parseToolInput(tool, completionToolCall.args);
if (abortSignal?.aborted === true)
return createErrorToolResult(completionToolCall, parsedArgs, abortSignal.reason, abortSignal);
shouldPostToolCall = true;
- const result = await tool.execute(parsedArgs, toolExecuteOptions);
+ let result;
+ try {
+ result = await tool.execute(parsedArgs, toolExecuteOptions);
+ } catch (cause) {
+ if (isAbortError(cause, abortSignal))
+ throw cause;
+ throw new ToolExecutionError(`Tool "${completionToolCall.toolName}" execution failed.`, {
+ cause,
+ toolCallId: completionToolCall.toolCallId,
+ toolInput: parsedArgs,
+ toolName: completionToolCall.toolName
+ });
+ }
return {
args: parsedArgs,
result,
@@ -144,7 +160,7 @@ const executeTool = async ({ abortSignal, messages, postToolCall, preToolCall, t
});
if (shouldPostToolCall) {
completionToolResult.args = parsedArgs;
- const postToolCallResult = await catchToolError(completionToolResult, abortSignal, async (toolResult) => postToolCall?.(toolResult, toolExecuteOptions));
+ const postToolCallResult = await catchToolError(completionToolResult, abortSignal, captureToolErrors, async (toolResult) => postToolCall?.(toolResult, toolExecuteOptions));
if (postToolCallResult) {
assertSameToolCallId(completionToolResult.toolCallId, postToolCallResult, "postToolCallResult");
completionToolResult = postToolCallResult;
@@ -156,6 +172,54 @@ const executeTool = async ({ abortSignal, messages, postToolCall, preToolCall, t
result: wrap(completionToolResult.result)
};
};
+const callToolLifecycle = async (callback, context) => {
+ try {
+ await callback?.(context);
+ } catch {
+ }
+};
+const executeTool = async (options) => {
+ const { abortSignal, captureToolErrors, messages, onToolCallFinish, onToolCallStart, repairToolCall, toolCall, tools } = options;
+ const toolCallId = toolCall.id;
+ const toolName = toolCall.function?.name ?? "unknown";
+ const startTime = Date.now();
+ try {
+ const execution = await executeToolBase(options);
+ const failure = execution.completionToolResult.error;
+ if (failure instanceof Error && repairToolCall && (InvalidToolCallError.isInstance(failure) || InvalidToolInputError.isInstance(failure))) {
+ const repaired = await repairToolCall({ error: failure, messages, toolCall, tools });
+ if (repaired != null)
+ return executeTool({ ...options, repairToolCall: void 0, toolCall: repaired });
+ }
+ if (!execution.completionToolResult.isError)
+ await callToolLifecycle(onToolCallStart, { input: execution.completionToolResult.args, toolCallId, toolName });
+ await callToolLifecycle(onToolCallFinish, {
+ durationMs: Date.now() - startTime,
+ error: execution.completionToolResult.error,
+ output: execution.completionToolResult.isError ? void 0 : execution.completionToolResult.result,
+ toolCallId,
+ toolName
+ });
+ return {
+ ...execution,
+ message: {
+ content: execution.result,
+ role: "tool",
+ tool_call_id: execution.completionToolCall.toolCallId
+ }
+ };
+ } catch (error) {
+ if (isAbortError(error, abortSignal))
+ throw error;
+ if (error instanceof Error && repairToolCall && (InvalidToolCallError.isInstance(error) || InvalidToolInputError.isInstance(error))) {
+ const repaired = await repairToolCall({ error, messages, toolCall, tools });
+ if (repaired != null)
+ return executeTool({ ...options, repairToolCall: void 0, toolCall: repaired });
+ }
+ await callToolLifecycle(onToolCallFinish, { durationMs: Date.now() - startTime, error, toolCallId, toolName });
+ throw error;
+ }
+};
const resolvePrepareStep = async ({ input, model, prepareStep, stepNumber, steps, toolChoice }) => {
const prepared = prepareStep == null ? void 0 : await prepareStep({
@@ -1,200 +0,0 @@
diff --git a/dist/index.d.ts b/dist/index.d.ts
index a596849dec1316dd84e4261650aa6b439327a4f5..b238d42480e4654cdb89d0619540275b7796e80d 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,5 +1,35 @@
import { WithUnknown } from '@xsai/shared';
-import { ToolCall, FinishReason, ChatCompletionUsage, ChatOptions, Event, CompletionStep, PostToolCall, PrepareStep, PreToolCall, StopCondition, Message, Usage } from '@xsai/shared-chat';
+import { CompletionToolCall, CompletionToolResult, ToolCall, FinishReason, ChatCompletionUsage, ChatOptions, CompletionStep, OnToolCallFinishCallback, OnToolCallStartCallback, PostToolCall, PrepareStep, PreToolCall, RepairToolCallFunction, StopCondition, Message, Usage } from '@xsai/shared-chat';
+
+type StreamTextEvent = (CompletionToolCall & {
+ type: 'tool-call';
+}) | (CompletionToolResult & {
+ type: 'tool-error';
+}) | (CompletionToolResult & {
+ type: 'tool-result';
+}) | {
+ argsTextDelta: string;
+ toolCallId: string;
+ toolName: string;
+ type: 'tool-call-delta';
+} | {
+ error: unknown;
+ type: 'error';
+} | {
+ finishReason: FinishReason;
+ type: 'finish';
+ usage?: Usage;
+} | {
+ text: string;
+ type: 'reasoning-delta';
+} | {
+ text: string;
+ type: 'text-delta';
+} | {
+ toolCallId: string;
+ toolName: string;
+ type: 'tool-call-streaming-start';
+};
interface StreamTextChunkResult {
choices: {
@@ -27,12 +57,16 @@ interface StreamTextChunkResult {
}
interface StreamTextOptions extends ChatOptions {
- onEvent?: (event: Event) => Promise<unknown> | unknown;
+ onEvent?: (event: StreamTextEvent) => Promise<unknown> | unknown;
onFinish?: (step?: CompletionStep) => Promise<unknown> | unknown;
onStepFinish?: (step: CompletionStep) => Promise<unknown> | unknown;
+ captureToolErrors?: boolean;
+ onToolCallFinish?: OnToolCallFinishCallback;
+ onToolCallStart?: OnToolCallStartCallback;
postToolCall?: PostToolCall;
prepareStep?: PrepareStep;
preToolCall?: PreToolCall;
+ repairToolCall?: RepairToolCallFunction;
/** @default `stepCountAtLeast(1)` */
stopWhen?: StopCondition<Message>;
/**
@@ -48,8 +82,8 @@ interface StreamTextOptions extends ChatOptions {
};
}
interface StreamTextResult {
- eventStream: ReadableStream<Event>;
- fullStream: ReadableStream<StreamTextChunkResult>;
+ eventStream: ReadableStream<StreamTextEvent>;
+ fullStream: ReadableStream<StreamTextEvent>;
messages: Promise<Message[]>;
reasoningTextStream: ReadableStream<string>;
steps: Promise<CompletionStep[]>;
@@ -60,4 +94,4 @@ interface StreamTextResult {
declare const streamText: (options: WithUnknown<StreamTextOptions>) => StreamTextResult;
export { streamText };
-export type { StreamTextChunkResult, StreamTextOptions, StreamTextResult };
+export type { StreamTextChunkResult, StreamTextEvent, StreamTextOptions, StreamTextResult };
diff --git a/dist/index.js b/dist/index.js
index ec6e01b0defd49370f75ed50d975aa6da8c03faf..4a956cc07f62049de4fe2ea10ba8c3fba2875ecb 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -63,7 +63,6 @@ const streamText = (options) => {
let finishReason = "other";
let reasoningStarted = false;
let textStarted = false;
- pushEvent({ type: "step.start" });
await stream.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()).pipeThrough(new JsonMessageTransformStream()).pipeTo(new WritableStream({
abort: (reason) => {
errorControllers(reason, eventCtrl, fullCtrl, textCtrl, reasoningTextCtrl);
@@ -82,18 +81,16 @@ const streamText = (options) => {
reasoningField = "reasoning";
if (!reasoningStarted) {
reasoningStarted = true;
- pushEvent({ type: "reasoning.start" });
}
- pushEvent({ delta: choice.delta.reasoning, type: "reasoning.delta" });
+ pushEvent({ text: choice.delta.reasoning, type: "reasoning-delta" });
pushReasoningText(choice.delta.reasoning);
} else if (choice.delta.reasoning_content != null) {
if (reasoningField !== "reasoning_content")
reasoningField = "reasoning_content";
if (!reasoningStarted) {
reasoningStarted = true;
- pushEvent({ type: "reasoning.start" });
}
- pushEvent({ delta: choice.delta.reasoning_content, type: "reasoning.delta" });
+ pushEvent({ text: choice.delta.reasoning_content, type: "reasoning-delta" });
pushReasoningText(choice.delta.reasoning_content);
}
if (choice.finish_reason != null)
@@ -102,16 +99,14 @@ const streamText = (options) => {
if (choice.delta.content != null) {
if (!textStarted) {
textStarted = true;
- pushEvent({ type: "text.start" });
}
- pushEvent({ delta: choice.delta.content, type: "text.delta" });
+ pushEvent({ text: choice.delta.content, type: "text-delta" });
pushText(choice.delta.content);
} else if (choice.delta.refusal != null) {
if (!textStarted) {
textStarted = true;
- pushEvent({ type: "text.start" });
}
- pushEvent({ delta: choice.delta.refusal, type: "text.delta" });
+ pushEvent({ text: choice.delta.refusal, type: "text-delta" });
pushText(choice.delta.refusal);
}
} else {
@@ -125,21 +120,17 @@ const streamText = (options) => {
arguments: toolCall.function.arguments ?? ""
}
};
- pushEvent({ toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call.start" });
+ pushEvent({ toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-streaming-start" });
if (toolCall.function.arguments != null && toolCall.function.arguments.length > 0)
- pushEvent({ delta: toolCall.function.arguments, type: "tool-call.delta" });
+ pushEvent({ argsTextDelta: toolCall.function.arguments, toolCallId: toolCall.id, toolName: toolCall.function.name, type: "tool-call-delta" });
} else {
tool_calls[index].function.arguments += toolCall.function.arguments;
- pushEvent({ delta: toolCall.function.arguments, type: "tool-call.delta" });
+ pushEvent({ argsTextDelta: toolCall.function.arguments, toolCallId: toolCall.id, toolName: toolCall.function.name ?? tool_calls[index].function.name, type: "tool-call-delta" });
}
}
}
}
}));
- if (reasoningStarted)
- pushEvent({ content: reasoningText ?? "", type: "reasoning.done" });
- if (textStarted)
- pushEvent({ content: text, type: "text.done" });
messages.push({
...reasoningField != null ? { [reasoningField]: reasoningText } : {},
content: text,
@@ -152,7 +143,7 @@ const streamText = (options) => {
if (options.abortSignal?.aborted === true)
throw options.abortSignal.reason ?? new Error("This operation was aborted");
for (const toolCall of toolCalls)
- pushEvent({ ...toolCall, type: "tool-call.done" });
+ pushEvent({ ...toolCall, type: "tool-call" });
const step = {
finishReason,
text,
@@ -169,9 +160,13 @@ const streamText = (options) => {
const results = await Promise.all(
validToolCalls.map(async (toolCall) => executeTool({
abortSignal: options.abortSignal,
+ captureToolErrors: options.captureToolErrors,
messages,
+ onToolCallFinish: options.onToolCallFinish,
+ onToolCallStart: options.onToolCallStart,
postToolCall: options.postToolCall,
preToolCall: options.preToolCall,
+ repairToolCall: options.repairToolCall,
toolCall,
tools: options.tools
}))
@@ -185,12 +180,12 @@ const streamText = (options) => {
role: "tool",
tool_call_id: completionToolCall.toolCallId
});
- pushEvent({ ...completionToolResult, type: "tool-result.done" });
+ pushEvent({ ...completionToolResult, type: completionToolResult.isError ? "tool-error" : "tool-result" });
}
}
const willContinue = validToolCalls.length > 0 && !stop && !options.abortSignal?.aborted;
pushStep(step);
- pushEvent({ type: "step.done", usage });
+ pushEvent({ finishReason, type: "finish", usage });
if (willContinue)
return async () => doStream();
};
@@ -222,7 +217,7 @@ const streamText = (options) => {
})();
return {
eventStream,
- fullStream,
+ fullStream: eventStream,
messages: resultMessages.promise,
reasoningTextStream,
steps: resultSteps.promise,
+56 -94
View File
@@ -1162,8 +1162,8 @@ catalogs:
specifier: ^4.0.0
version: 4.0.0
xsschema:
specifier: 0.5.0-beta.2
version: 0.5.0-beta.2
specifier: 0.5.0-beta.8
version: 0.5.0-beta.8
yaml:
specifier: ^2.8.3
version: 2.8.3
@@ -1205,15 +1205,6 @@ overrides:
packageExtensionsChecksum: sha256-lLxz4me6BFXSsuohDen54RU9DmiItse34Xa+IWGtSzU=
patchedDependencies:
'@xsai/generate-text@0.5.0-beta.8':
hash: 9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6
path: patches/@xsai__generate-text@0.5.0-beta.8.patch
'@xsai/shared-chat@0.5.0-beta.8':
hash: dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50
path: patches/@xsai__shared-chat@0.5.0-beta.8.patch
'@xsai/stream-text@0.5.0-beta.8':
hash: 55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229
path: patches/@xsai__stream-text@0.5.0-beta.8.patch
mineflayer-pathfinder:
hash: 4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797
path: patches/mineflayer-pathfinder.patch
@@ -1383,13 +1374,13 @@ importers:
version: 14.2.1(vue@3.5.32(typescript@5.9.3))
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
valibot:
specifier: 'catalog:'
version: 1.4.2(typescript@5.9.3)
@@ -1398,7 +1389,7 @@ importers:
version: 3.5.32(typescript@5.9.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
devDependencies:
'@iconify-json/solar':
specifier: 'catalog:'
@@ -1537,7 +1528,7 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/model':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -1546,10 +1537,10 @@ importers:
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
'@xsai/stream-transcription':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -1678,7 +1669,7 @@ importers:
version: 7.4.0
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
yauzl:
specifier: 'catalog:'
version: 3.3.0
@@ -1958,7 +1949,7 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/model':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -1967,10 +1958,10 @@ importers:
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
'@xsai/stream-transcription':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -2123,7 +2114,7 @@ importers:
version: 5.0.0
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -2433,7 +2424,7 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/model':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -2442,10 +2433,10 @@ importers:
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
'@xsai/stream-transcription':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -2577,7 +2568,7 @@ importers:
version: 7.4.0
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
yauzl:
specifier: 'catalog:'
version: 3.3.0
@@ -3080,13 +3071,13 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/generate-transcription':
specifier: 'catalog:'
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
discord.js:
specifier: 'catalog:'
version: 14.26.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)
@@ -3119,10 +3110,10 @@ importers:
version: link:../../packages/server-sdk
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
alien-signals:
specifier: 'catalog:'
version: 3.1.2
@@ -3228,10 +3219,10 @@ importers:
version: 0.4.0(typescript@5.9.3)
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/utils-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -3319,10 +3310,10 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/tool':
specifier: 'catalog:'
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
@@ -3550,7 +3541,7 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/model':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -3559,10 +3550,10 @@ importers:
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
'@xsai/tool':
specifier: 'catalog:'
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
@@ -3586,7 +3577,7 @@ importers:
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -3788,7 +3779,7 @@ importers:
version: 1.0.0-beta.14(electron@41.2.1)(h3@2.0.1-rc.25)(hono@4.11.3)(web-worker@1.5.0)
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
packages/plugin-sdk:
dependencies:
@@ -3828,7 +3819,7 @@ importers:
version: 1.4.2(typescript@5.9.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
packages/scenarios-stage-tamagotchi-browser:
dependencies:
@@ -4172,7 +4163,7 @@ importers:
version: 2.0.9(vue@3.5.32(typescript@5.9.3))
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -4369,7 +4360,7 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/generate-transcription':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -4381,10 +4372,10 @@ importers:
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
'@xsai/stream-transcription':
specifier: 'catalog:'
version: 0.5.0-beta.8
@@ -4537,7 +4528,7 @@ importers:
version: 4.0.0
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
zod:
specifier: 'catalog:'
version: 4.4.3
@@ -5243,13 +5234,13 @@ importers:
version: 0.5.0-beta.8
'@xsai/generate-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
version: 0.5.0-beta.8
'@xsai/shared-chat':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
version: 0.5.0-beta.8
'@xsai/stream-text':
specifier: 'catalog:'
version: 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
version: 0.5.0-beta.8
pinia:
specifier: 'catalog:'
version: 3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3))
@@ -5261,7 +5252,7 @@ importers:
version: 0.0.5(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@2.0.1)(canvas@3.2.3))(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
version: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
plugins/airi-plugin-homeassistant:
dependencies:
@@ -19841,29 +19832,6 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
xsschema@0.5.0-beta.2:
resolution: {integrity: sha512-zUtk3Ro6Gn39zn7fJRImwmLJmSOpao09dUxdLVyofr+RzfxEEdtm7UVJPe4lUGTJxUCzNptpRp549yD9uKWiug==}
peerDependencies:
'@valibot/to-json-schema': ^1.0.0
arktype: ^2.1.20
effect: ^3.16.0
sury: ^10.0.0 || ^11.0.0-alpha
zod: ^3.25.0 || ^4.0.0
zod-to-json-schema: ^3.25.0
peerDependenciesMeta:
'@valibot/to-json-schema':
optional: true
arktype:
optional: true
effect:
optional: true
sury:
optional: true
zod:
optional: true
zod-to-json-schema:
optional: true
xsschema@0.5.0-beta.8:
resolution: {integrity: sha512-y74st5jRCpO/EMhxi4MC2/GKpWVL2LL12utk5QK2ICCEXVOV6BvOLYp6dXkJBIb4BUCfaVCEei5/cY+JTv3CRw==}
peerDependencies:
@@ -20082,9 +20050,9 @@ snapshots:
dependencies:
'@moeru/std': 0.1.0-beta.20
'@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.9.3))
'@xsai/generate-text': 0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/stream-text': 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
'@xsai/generate-text': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8
'@xsai/stream-text': 0.5.0-beta.8
'@xsai/tool': 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
es-toolkit: 1.50.0
minimatch: 10.2.6
@@ -20224,8 +20192,8 @@ snapshots:
dependencies:
'@xsai-ext/responses': 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/stream-text': 0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)
'@xsai/shared-chat': 0.5.0-beta.8
'@xsai/stream-text': 0.5.0-beta.8
'@xsai/tool': 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
yocto-queue: 1.2.2
transitivePeerDependencies:
@@ -27191,7 +27159,7 @@ snapshots:
'@xsai-ext/responses@0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)':
dependencies:
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/shared-chat': 0.5.0-beta.8
'@xsai/shared-stream': 0.5.0-beta.8
'@xsai/tool': 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
transitivePeerDependencies:
@@ -27267,12 +27235,12 @@ snapshots:
'@xsai/generate-text@0.5.0-beta.2':
dependencies:
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/shared-chat': 0.5.0-beta.8
'@xsai/generate-text@0.5.0-beta.8(patch_hash=9ff1a2c30f41026ddc970aec81e65e1d8073abd1ec2387bc4f017f4ccef65fc6)':
'@xsai/generate-text@0.5.0-beta.8':
dependencies:
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/shared-chat': 0.5.0-beta.8
'@xsai/generate-transcription@0.4.4':
dependencies:
@@ -27286,7 +27254,7 @@ snapshots:
dependencies:
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat@0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)':
'@xsai/shared-chat@0.5.0-beta.8':
dependencies:
'@xsai/shared': 0.5.0-beta.8
@@ -27298,10 +27266,10 @@ snapshots:
'@xsai/shared@0.5.0-beta.8': {}
'@xsai/stream-text@0.5.0-beta.8(patch_hash=55bbd5398e6758eb67e496fa21571cc2623ef2e91d7adc7fdee3ec1a13bd6229)':
'@xsai/stream-text@0.5.0-beta.8':
dependencies:
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/shared-chat': 0.5.0-beta.8
'@xsai/shared-stream': 0.5.0-beta.8
'@xsai/stream-transcription@0.5.0-beta.8':
@@ -27312,7 +27280,7 @@ snapshots:
'@xsai/tool@0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)':
dependencies:
'@xsai/shared': 0.5.0-beta.8
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/shared-chat': 0.5.0-beta.8
xsschema: 0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)
transitivePeerDependencies:
- '@valibot/to-json-schema'
@@ -27324,7 +27292,7 @@ snapshots:
'@xsai/utils-chat@0.5.0-beta.8':
dependencies:
'@xsai/shared-chat': 0.5.0-beta.8(patch_hash=dc3d30181c5826cac0535c0885fcea4b4ea70651da3b28ff593574034718cf50)
'@xsai/shared-chat': 0.5.0-beta.8
abbrev@2.0.0: {}
@@ -36558,18 +36526,12 @@ snapshots:
xmlhttprequest-ssl@2.1.2: {}
xsschema@0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3):
xsschema@0.5.0-beta.8(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3):
optionalDependencies:
'@valibot/to-json-schema': 1.0.0-rc.0(valibot@1.4.2(typescript@5.9.3))
zod: 4.4.3
zod-to-json-schema: 3.25.2(zod@4.4.3)
xsschema@0.5.0-beta.2(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3):
optionalDependencies:
'@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.9.3))
zod: 4.4.3
zod-to-json-schema: 3.25.2(zod@4.4.3)
xsschema@0.5.0-beta.8(@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3):
optionalDependencies:
'@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.9.3))
+1 -4
View File
@@ -31,9 +31,6 @@ overrides:
side-channel: npm:@nolyfill/side-channel@^1.0.44
string.prototype.matchall: npm:@nolyfill/string.prototype.matchall@^1.0.44
patchedDependencies:
'@xsai/generate-text@0.5.0-beta.8': patches/@xsai__generate-text@0.5.0-beta.8.patch
'@xsai/shared-chat@0.5.0-beta.8': patches/@xsai__shared-chat@0.5.0-beta.8.patch
'@xsai/stream-text@0.5.0-beta.8': patches/@xsai__stream-text@0.5.0-beta.8.patch
mineflayer-pathfinder: patches/mineflayer-pathfinder.patch
pixi-live2d-display: patches/pixi-live2d-display.patch
sponsorkit@17.1.0: patches/sponsorkit@17.1.0.patch
@@ -425,7 +422,7 @@ catalog:
wxt: ^0.20.24
xast-util-to-xml: ^4.0.0
xastscript: ^4.0.0
xsschema: 0.5.0-beta.2
xsschema: 0.5.0-beta.8
yaml: ^2.8.3
yauzl: ^3.3.0
zod: ^4.3.6