Skip to content

Commit 30e982a

Browse files
aronlgrammel
andcommitted
feat(ai): extend addToolResult to support error results
This commit extends the `addToolResult` method on the `AbstractChat` class to optionally take an `output-error` result. This allows clients to report both successful and failed tool calls using the same mechanism. addToolResult({ state: "output-error", errorText: "Failed" }); For backwards compatibility the existing method interface is still supported and will default to a state of `output-available`. Co-Authored-By: Lars Grammel <[email protected]>
1 parent ecb0152 commit 30e982a

File tree

2 files changed

+167
-10
lines changed

2 files changed

+167
-10
lines changed

packages/ai/src/ui/chat.test.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,149 @@ describe('Chat', () => {
17421742
`);
17431743
});
17441744

1745+
it('should send message when a tool error result is submitted', async () => {
1746+
server.urls['http://localhost:3000/api/chat'].response = [
1747+
{
1748+
type: 'stream-chunks',
1749+
chunks: [
1750+
formatChunk({ type: 'start' }),
1751+
formatChunk({ type: 'start-step' }),
1752+
formatChunk({
1753+
type: 'tool-input-available',
1754+
toolCallId: 'tool-call-0',
1755+
toolName: 'test-tool',
1756+
input: { testArg: 'test-value' },
1757+
}),
1758+
formatChunk({ type: 'finish-step' }),
1759+
formatChunk({ type: 'finish' }),
1760+
],
1761+
},
1762+
{
1763+
type: 'stream-chunks',
1764+
chunks: [
1765+
formatChunk({ type: 'start' }),
1766+
formatChunk({ type: 'start-step' }),
1767+
formatChunk({ type: 'finish-step' }),
1768+
formatChunk({ type: 'finish' }),
1769+
],
1770+
},
1771+
];
1772+
1773+
let callCount = 0;
1774+
const onFinishPromise = createResolvablePromise<void>();
1775+
1776+
const chat = new TestChat({
1777+
id: '123',
1778+
generateId: mockId(),
1779+
transport: new DefaultChatTransport({
1780+
api: 'http://localhost:3000/api/chat',
1781+
}),
1782+
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
1783+
onFinish: () => {
1784+
callCount++;
1785+
if (callCount === 2) {
1786+
onFinishPromise.resolve();
1787+
}
1788+
},
1789+
});
1790+
1791+
await chat.sendMessage({
1792+
text: 'Hello, world!',
1793+
});
1794+
1795+
// user submits the tool result
1796+
await chat.addToolResult({
1797+
state: 'output-error',
1798+
tool: 'test-tool',
1799+
toolCallId: 'tool-call-0',
1800+
errorText: 'test-error',
1801+
});
1802+
1803+
// UI should show the tool result
1804+
expect(chat.messages).toMatchInlineSnapshot(`
1805+
[
1806+
{
1807+
"id": "id-0",
1808+
"metadata": undefined,
1809+
"parts": [
1810+
{
1811+
"text": "Hello, world!",
1812+
"type": "text",
1813+
},
1814+
],
1815+
"role": "user",
1816+
},
1817+
{
1818+
"id": "id-1",
1819+
"metadata": undefined,
1820+
"parts": [
1821+
{
1822+
"type": "step-start",
1823+
},
1824+
{
1825+
"errorText": "test-error",
1826+
"input": {
1827+
"testArg": "test-value",
1828+
},
1829+
"output": undefined,
1830+
"preliminary": undefined,
1831+
"providerExecuted": undefined,
1832+
"rawInput": undefined,
1833+
"state": "output-error",
1834+
"toolCallId": "tool-call-0",
1835+
"type": "tool-test-tool",
1836+
},
1837+
],
1838+
"role": "assistant",
1839+
},
1840+
]
1841+
`);
1842+
1843+
await onFinishPromise.promise;
1844+
1845+
// 2nd call should happen after the stream is finished
1846+
expect(server.calls.length).toBe(2);
1847+
1848+
// check details of the 2nd call
1849+
expect(await server.calls[1].requestBodyJson).toMatchInlineSnapshot(`
1850+
{
1851+
"id": "123",
1852+
"messageId": "id-1",
1853+
"messages": [
1854+
{
1855+
"id": "id-0",
1856+
"parts": [
1857+
{
1858+
"text": "Hello, world!",
1859+
"type": "text",
1860+
},
1861+
],
1862+
"role": "user",
1863+
},
1864+
{
1865+
"id": "id-1",
1866+
"parts": [
1867+
{
1868+
"type": "step-start",
1869+
},
1870+
{
1871+
"errorText": "test-error",
1872+
"input": {
1873+
"testArg": "test-value",
1874+
},
1875+
"state": "output-error",
1876+
"toolCallId": "tool-call-0",
1877+
"type": "tool-test-tool",
1878+
},
1879+
],
1880+
"role": "assistant",
1881+
},
1882+
],
1883+
"trigger": "submit-message",
1884+
}
1885+
`);
1886+
});
1887+
17451888
it('should send message when a dynamic tool result is submitted', async () => {
17461889
server.urls['http://localhost:3000/api/chat'].response = [
17471890
{
@@ -1795,6 +1938,7 @@ describe('Chat', () => {
17951938

17961939
// user submits the tool result
17971940
await chat.addToolResult({
1941+
state: 'output-available',
17981942
tool: 'test-tool',
17991943
toolCallId: 'tool-call-0',
18001944
output: 'test-result',

packages/ai/src/ui/chat.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import {
1919
InferUIMessageToolCall,
2020
isToolOrDynamicToolUIPart,
21+
ToolUIPart,
2122
type DataUIPart,
2223
type FileUIPart,
2324
type InferUIMessageData,
@@ -414,14 +415,26 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
414415
};
415416

416417
addToolResult = async <TOOL extends keyof InferUIMessageTools<UI_MESSAGE>>({
418+
state = 'output-available',
417419
tool,
418420
toolCallId,
419421
output,
420-
}: {
421-
tool: TOOL;
422-
toolCallId: string;
423-
output: InferUIMessageTools<UI_MESSAGE>[TOOL]['output'];
424-
}) =>
422+
errorText,
423+
}:
424+
| {
425+
state?: 'output-available';
426+
tool: TOOL;
427+
toolCallId: string;
428+
output: InferUIMessageTools<UI_MESSAGE>[TOOL]['output'];
429+
errorText?: never;
430+
}
431+
| {
432+
state: 'output-error';
433+
tool: TOOL;
434+
toolCallId: string;
435+
output?: never;
436+
errorText: string;
437+
}) =>
425438
this.jobExecutor.run(async () => {
426439
const messages = this.state.messages;
427440
const lastMessage = messages[messages.length - 1];
@@ -430,7 +443,7 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
430443
...lastMessage,
431444
parts: lastMessage.parts.map(part =>
432445
isToolOrDynamicToolUIPart(part) && part.toolCallId === toolCallId
433-
? { ...part, state: 'output-available', output }
446+
? { ...part, state, output, errorText }
434447
: part,
435448
),
436449
});
@@ -440,12 +453,12 @@ export abstract class AbstractChat<UI_MESSAGE extends UIMessage> {
440453
this.activeResponse.state.message.parts =
441454
this.activeResponse.state.message.parts.map(part =>
442455
isToolOrDynamicToolUIPart(part) && part.toolCallId === toolCallId
443-
? {
456+
? ({
444457
...part,
445-
state: 'output-available',
458+
state,
446459
output,
447-
errorText: undefined,
448-
}
460+
errorText,
461+
} as ToolUIPart<InferUIMessageTools<UI_MESSAGE>>)
449462
: part,
450463
);
451464
}

0 commit comments

Comments
 (0)