Skip to content

Commit 4a14253

Browse files
committed
fix(client): correctly track input from server_tool_use input deltas
1 parent 3c70ae3 commit 4a14253

File tree

3 files changed

+90
-4
lines changed

3 files changed

+90
-4
lines changed

src/lib/BetaMessageStream.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
type MessageCreateParamsBase as BetaMessageCreateParamsBase,
1111
type BetaTextBlock,
1212
type BetaTextCitation,
13+
type BetaToolUseBlock,
14+
type BetaServerToolUseBlock,
15+
type BetaMCPToolUseBlock,
1316
} from '../resources/beta/messages/messages';
1417
import { Stream } from '../streaming';
1518
import { partialParse } from '../_vendor/partial-json-parser/parser';
@@ -39,6 +42,18 @@ type MessageStreamEventListeners<Event extends keyof MessageStreamEvents> = {
3942

4043
const JSON_BUF_PROPERTY = '__json_buf';
4144

45+
/**
46+
* Types of content blocks that track tool input via input_json_delta events
47+
*/
48+
export type TracksToolInput = BetaToolUseBlock | BetaServerToolUseBlock | BetaMCPToolUseBlock;
49+
50+
/**
51+
* Type guard to check if a content block is one that tracks tool input
52+
*/
53+
function tracksToolInput(content: BetaContentBlock): content is TracksToolInput {
54+
return content.type === 'tool_use' || content.type === 'server_tool_use' || content.type === 'mcp_tool_use';
55+
}
56+
4257
export class BetaMessageStream implements AsyncIterable<BetaMessageStreamEvent> {
4358
messages: BetaMessageParam[] = [];
4459
receivedMessages: BetaMessage[] = [];
@@ -432,7 +447,7 @@ export class BetaMessageStream implements AsyncIterable<BetaMessageStreamEvent>
432447
break;
433448
}
434449
case 'input_json_delta': {
435-
if ((content.type === 'tool_use' || content.type === 'mcp_tool_use') && content.input) {
450+
if (tracksToolInput(content) && content.input) {
436451
this._emit('inputJson', event.delta.partial_json, content.input);
437452
}
438453
break;
@@ -571,7 +586,7 @@ export class BetaMessageStream implements AsyncIterable<BetaMessageStreamEvent>
571586
break;
572587
}
573588
case 'input_json_delta': {
574-
if (snapshotContent?.type === 'tool_use' || snapshotContent?.type === 'mcp_tool_use') {
589+
if (snapshotContent && tracksToolInput(snapshotContent)) {
575590
// we need to keep track of the raw JSON string as well so that we can
576591
// re-parse it for each delta, for now we just store it as an untyped
577592
// non-enumerable property on the snapshot

src/lib/MessageStream.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
type MessageCreateParamsBase,
1111
type TextBlock,
1212
type TextCitation,
13+
type ToolUseBlock,
14+
type ServerToolUseBlock,
1315
} from '../resources/messages';
1416
import { Stream } from '../streaming';
1517
import { partialParse } from '../_vendor/partial-json-parser/parser';
@@ -39,6 +41,19 @@ type MessageStreamEventListeners<Event extends keyof MessageStreamEvents> = {
3941

4042
const JSON_BUF_PROPERTY = '__json_buf';
4143

44+
/**
45+
* Types of content blocks that track tool input via input_json_delta events
46+
*/
47+
export type TracksToolInput = ToolUseBlock | ServerToolUseBlock;
48+
49+
/**
50+
* Type guard to check if a content block is one that tracks tool input
51+
* by having an `input` property that is updated via input_json_delta events.
52+
*/
53+
function tracksToolInput(content: ContentBlock): content is TracksToolInput {
54+
return content.type === 'tool_use' || content.type === 'server_tool_use';
55+
}
56+
4257
export class MessageStream implements AsyncIterable<MessageStreamEvent> {
4358
messages: MessageParam[] = [];
4459
receivedMessages: Message[] = [];
@@ -432,7 +447,7 @@ export class MessageStream implements AsyncIterable<MessageStreamEvent> {
432447
break;
433448
}
434449
case 'input_json_delta': {
435-
if (content.type === 'tool_use' && content.input) {
450+
if (tracksToolInput(content) && content.input) {
436451
this._emit('inputJson', event.delta.partial_json, content.input);
437452
}
438453
break;
@@ -571,7 +586,7 @@ export class MessageStream implements AsyncIterable<MessageStreamEvent> {
571586
break;
572587
}
573588
case 'input_json_delta': {
574-
if (snapshotContent?.type === 'tool_use') {
589+
if (snapshotContent && tracksToolInput(snapshotContent)) {
575590
// we need to keep track of the raw JSON string as well so that we can
576591
// re-parse it for each delta, for now we just store it as an untyped
577592
// non-enumerable property on the snapshot

tests/lib/TracksToolInput.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { ContentBlock } from '../../src/resources/messages';
2+
3+
import { TracksToolInput } from '@anthropic-ai/sdk/lib/MessageStream';
4+
import { TracksToolInput as BetaTracksToolInput } from '@anthropic-ai/sdk/lib/BetaMessageStream';
5+
import { BetaContentBlock } from '@anthropic-ai/sdk/resources/beta';
6+
7+
/**
8+
* This test ensures that our TracksToolInput type includes all content block types that have an input property.
9+
* If any new content block types with input properties are added, they should be added to the TracksToolInput types.
10+
*/
11+
12+
describe('TracksToolInput type', () => {
13+
describe('Regular MessageStream', () => {
14+
type ContentBlockWithInput = Extract<ContentBlock, { input: unknown }>;
15+
16+
it('TracksToolInput includes all content block types with input properties', () => {
17+
// Define a type that extracts all ContentBlock types that have an input property
18+
19+
// TypeScript compile-time check: all ContentBlock types with input property
20+
// should be assignable to TracksToolInput
21+
type Test = ContentBlockWithInput extends TracksToolInput ? true : false;
22+
const test: Test = true;
23+
expect(test).toBe(true);
24+
});
25+
26+
it('all TracksToolInput types should have an input property', () => {
27+
// TypeScript compile-time check: all TracksToolInput types should have
28+
// an input property (i.e., be assignable to ContentBlockWithInput)
29+
type Test2 = TracksToolInput extends ContentBlockWithInput ? true : false;
30+
const test2: Test2 = true;
31+
expect(test2).toBe(true);
32+
});
33+
});
34+
35+
describe('Beta MessageStream', () => {
36+
type BetaContentBlockWithInput = Extract<BetaContentBlock, { input: unknown }>;
37+
38+
it('TracksToolInput includes all content block types with input properties', () => {
39+
// Define a type that extracts all BetaContentBlock types that have an input property
40+
41+
// TypeScript compile-time check: all BetaContentBlock types with input property
42+
// should be assignable to BetaTracksToolInput
43+
type Test = BetaContentBlockWithInput extends BetaTracksToolInput ? true : false;
44+
const test: Test = true;
45+
expect(test).toBe(true);
46+
});
47+
48+
it('all BetaTracksToolInput types should have an input property', () => {
49+
// TypeScript compile-time check: all BetaTracksToolInput types should have
50+
// an input property (i.e., be assignable to BetaContentBlockWithInput)
51+
type Test2 = BetaTracksToolInput extends BetaContentBlockWithInput ? true : false;
52+
const test2: Test2 = true;
53+
expect(test2).toBe(true);
54+
});
55+
});
56+
});

0 commit comments

Comments
 (0)