GitHub Issue: #36 - Intermediate tool calls Requested by: @Evanfeenstra Date: August 22, 2025
The user is requesting support for intermediate tool calls in streamText and streamObject to enable building UIs that show tool calls and results as they happen, similar to other AI SDK providers.
The provider successfully implements streaming for:
- Text content (
text-start,text-delta,text-end) - Response metadata and session information
- Usage statistics and finish reasons
- Warnings and error handling
Tool-related streaming events are not emitted, even though tools are being called behind the scenes. The user only sees:
startstart-steptext-starttext-deltatext-end
Missing events that should be emitted:
tool-input-starttool-input-deltatool-input-endtool-calltool-result
The @anthropic-ai/claude-code SDK (v1.0.81) provides real-time streaming of all messages including:
// When Claude decides to use a tool:
{
type: 'assistant',
message: {
content: [
{
type: 'tool_use',
id: 'toolu_xxx',
name: 'ToolName',
input: { /* tool arguments */ }
}
]
}
}
// When tool execution completes:
{
type: 'user',
message: {
content: [
{
type: 'tool_result',
tool_use_id: 'toolu_xxx',
content: '/* tool output */',
is_error: false
}
]
}
}The AI SDK expects these LanguageModelV2StreamPart types for tools:
type ToolStreamParts =
| { type: 'tool-input-start'; id: string; toolName: string; }
| { type: 'tool-input-delta'; id: string; delta: string; }
| { type: 'tool-input-end'; id: string; }
| { type: 'tool-call'; toolCallId: string; toolName: string; input: string; }
| { type: 'tool-result'; toolCallId: string; result: unknown; }IMPORTANT: Claude Code's tools (Bash, Read, Write, etc.) are executed by the Claude Code SDK itself, NOT by the AI SDK's tool runner. We MUST set providerExecuted: true on all tool-call events to prevent the AI SDK from attempting to re-execute these tools. This is a critical distinction from user-defined tools that would normally be executed by the AI SDK.
File: src/claude-code-language-model.ts
// Add around line 190
private extractToolUses(content: any[]): Array<{id: string, name: string, input: any}> {
return content
.filter(c => c.type === 'tool_use')
.map(c => ({ id: c.id, name: c.name, input: c.input }));
}
private extractToolResults(content: any[]): Array<{id: string, result: string, isError: boolean}> {
return content
.filter(c => c.type === 'tool_result')
.map(c => ({ id: c.tool_use_id, result: c.content, isError: c.is_error }));
}for await (const message of response) {
if (message.type === 'assistant') {
// Extract text content
const textContent = message.message.content
.filter((c: any) => c.type === 'text')
.map((c: any) => c.text)
.join('');
// Extract tool uses
const toolUses = this.extractToolUses(message.message.content);
// Emit tool streaming events
for (const tool of toolUses) {
const toolId = generateId();
// Tool input start
controller.enqueue({
type: 'tool-input-start',
id: toolId,
toolName: tool.name,
});
// Tool input delta (send all at once since SDK doesn't stream incrementally)
const inputString = JSON.stringify(tool.input);
controller.enqueue({
type: 'tool-input-delta',
id: toolId,
delta: inputString,
});
// Tool input end
controller.enqueue({
type: 'tool-input-end',
id: toolId,
});
// Complete tool call
controller.enqueue({
type: 'tool-call',
toolCallId: tool.id,
toolName: tool.name,
input: inputString,
providerExecuted: true, // CRITICAL: Prevents AI SDK from re-executing Claude's internal tools
});
}
// Handle text content (existing code)
if (textContent) {
// ... existing text streaming logic
}
} else if (message.type === 'user') {
// Extract and emit tool results
const toolResults = this.extractToolResults(message.message.content);
for (const result of toolResults) {
controller.enqueue({
type: 'tool-result',
toolCallId: result.id,
result: result.result,
providerMetadata: {
'claude-code': {
isError: result.isError
}
}
});
}
}
// ... rest of existing code
}// examples/streaming-with-tools.ts
import { streamText } from 'ai';
import { claudeCode } from '../dist/index.js';
async function main() {
const result = streamText({
model: claudeCode('sonnet'),
prompt: 'What files are in the current directory? List them with their sizes.',
});
for await (const part of result.fullStream) {
switch(part.type) {
case 'tool-input-start':
console.log(`🔧 Starting tool: ${part.toolName}`);
break;
case 'tool-input-delta':
console.log(` Input: ${part.delta}`);
break;
case 'tool-call':
console.log(`✅ Tool called: ${part.toolName} (${part.toolCallId})`);
break;
case 'tool-result':
console.log(`📊 Tool result for ${part.toolCallId}:`, part.result?.substring(0, 100));
break;
case 'text-delta':
process.stdout.write(part.delta);
break;
}
}
}// examples/streaming-multiple-tools.ts
async function testMultipleTools() {
const result = streamText({
model: claudeCode('sonnet'),
prompt: 'Check the current directory, then read the package.json file, and summarize what this project does.',
});
const toolCalls = [];
const toolResults = [];
for await (const part of result.fullStream) {
if (part.type === 'tool-call') {
toolCalls.push({ name: part.toolName, id: part.toolCallId });
}
if (part.type === 'tool-result') {
toolResults.push({ id: part.toolCallId });
}
}
console.log('\nTool calls made:', toolCalls);
console.log('Tool results received:', toolResults);
}- Handle tool errors: When
is_error: truein tool_result - Handle missing tool results: Timeout or interrupted streams
- Handle malformed tool inputs: Invalid JSON in tool use
- Concurrent tool calls: Multiple tools called in same message
- Prevent double execution: MUST set
providerExecuted: trueon all tool-call events to prevent AI SDK from attempting to re-execute Claude Code's internal tools (Bash, Read, Write, etc.)
Update README.md with:
## Tool Streaming Support
The provider now supports streaming of intermediate tool calls, enabling real-time UI updates:
```typescript
const result = streamText({
model: claudeCode('sonnet'),
prompt: 'Analyze the current directory structure',
});
for await (const part of result.fullStream) {
switch(part.type) {
case 'tool-input-start':
// Tool call initiated
break;
case 'tool-input-delta':
// Tool input being streamed
break;
case 'tool-call':
// Complete tool call with ID and input
break;
case 'tool-result':
// Tool execution result
break;
// ... handle other stream parts
}
}- Day 1: Implement core tool detection and streaming logic
- Day 2: Add comprehensive test coverage
- Day 3: Handle edge cases and error scenarios
- Day 4: Update documentation and examples
- Day 5: Final testing and PR submission
This implementation is fully backwards compatible:
- Existing code continues to work unchanged
- New tool streaming events are additive only
- No breaking changes to existing APIs
- Users can opt-in to tool streaming by using
fullStream
- Enhanced UI/UX: Applications can show tool calls as they happen
- Better Debugging: Developers can see exactly what tools are being called
- Feature Parity: Matches capabilities of other AI SDK providers
- Progressive Enhancement: Works with existing code, enhances when needed
| Risk | Mitigation |
|---|---|
| Performance impact from additional events | Minimal - events are only emitted when tools are used |
| Breaking existing streams | Additive changes only, no modifications to existing events |
| Incomplete tool results | Add timeout handling and error states |
| Large tool inputs/outputs | Consider chunking for very large payloads |
| AI SDK re-executing provider tools | Set providerExecuted: true on all tool-call events |
| User confusion about tool source | Document that these are Claude Code's internal tools |
- Single tool call streams correctly
- Multiple sequential tool calls work
- Concurrent tool calls handled properly
- Tool errors are propagated correctly
- Stream interruption handled gracefully
- Large tool inputs/outputs work
- Backwards compatibility maintained
- Examples run successfully
- Documentation is clear and complete
- Review this plan with maintainers
- Create feature branch
feature/tool-streaming - Implement changes incrementally with tests
- Open PR with comprehensive description
- Address feedback and iterate