Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions strands-ts/src/tools/__tests__/tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ describe('FunctionTool', () => {
expect(receivedInput).toEqual(inputData)
})

it('handles null return values correctly', async () => {
it('handles null return values with empty content', async () => {
const tool = new FunctionTool({
name: 'nullTool',
description: 'Returns null',
Expand All @@ -273,16 +273,11 @@ describe('FunctionTool', () => {
type: 'toolResultBlock',
toolUseId: 'test-null',
status: 'success',
content: [
expect.objectContaining({
type: 'textBlock',
text: '<null>',
}),
],
content: [],
})
})

it('handles undefined return values correctly', async () => {
it('handles undefined return values with empty content', async () => {
const tool = new FunctionTool({
name: 'undefinedTool',
description: 'Returns undefined',
Expand All @@ -299,12 +294,32 @@ describe('FunctionTool', () => {
type: 'toolResultBlock',
toolUseId: 'test-undefined',
status: 'success',
content: [
expect.objectContaining({
type: 'textBlock',
text: '<undefined>',
}),
],
content: [],
})
})

it('handles void (no return) callbacks with empty content', async () => {
const tool = new FunctionTool({
name: 'voidTool',
description: 'Side-effectful tool with no return value',
inputSchema: { type: 'object', properties: { name: { type: 'string' } } },
// @ts-expect-error void is not assignable to JSONValue, but this is the real-world pattern for side-effectful tools
callback: (input: unknown): void => {
const { name } = input as { name: string }
// side effect only — console.log(`Hello, ${name}!`)
void name
},
})

const { result } = await collectGenerator(
tool.stream(createMockContext({ name: 'voidTool', toolUseId: 'test-void', input: { name: 'Alice' } }))
)

expect(result).toEqual({
type: 'toolResultBlock',
toolUseId: 'test-void',
status: 'success',
content: [],
})
})

Expand Down
18 changes: 5 additions & 13 deletions strands-ts/src/tools/function-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export class FunctionTool extends Tool implements InvokableTool<unknown, JSONVal
* - Arrays of content blocks → used directly as content array
* - Strings → TextBlock
* - Numbers, Booleans → TextBlock (converted to string)
* - null, undefined → TextBlock (special string representation)
* - null, undefined → empty content array (side-effectful tool with no return value)
* - Objects → JsonBlock (with deep copy)
* - Arrays (non-content blocks) → JsonBlock wrapped in \{ $value: array \} (with deep copy)
*
Expand All @@ -269,21 +269,13 @@ export class FunctionTool extends Tool implements InvokableTool<unknown, JSONVal
})
}

// Handle null with special string representation as text content
if (value === null) {
// Handle null/undefined (void tools) with empty content — side-effectful tools
// that don't return a value should signal success without a misleading placeholder.
if (value === null || value === undefined) {
return new ToolResultBlock({
toolUseId,
status: 'success',
content: [new TextBlock('<null>')],
})
}

// Handle undefined with special string representation as text content
if (value === undefined) {
return new ToolResultBlock({
toolUseId,
status: 'success',
content: [new TextBlock('<undefined>')],
content: [],
})
}

Expand Down