Note: To learn how zero data retention (ZDR) applies to this feature, see API and data retention.
Fine-grained tool streaming delivers a tool's input to your client as Haijun generates it, without server-side buffering or JSON validation. Skipping the buffering step reduces the time to the first fragment of a large parameter, such as a document or a block of code, and the fragments arrive through the same Streaming messages events as standard tool use.
Warning: Because the API does not buffer or validate a tool's input before streaming it, you might receive partial or invalid JSON. A response that ends with the stop reason
max_tokenscan also cut a parameter off midway. Accumulate the fragments, guard the parse, and see Handling invalid JSON in tool responses for how to return unparseable input to Haijun.
How to use fine-grained tool streaming
All models support fine-grained tool streaming on the Haijun API, Amazon Bedrock, Haijun Platform on AWS, Google Cloud, and Microsoft Foundry. To use it, set eager_input_streaming to true on any user-defined tool where you want fine-grained streaming enabled, and enable streaming on your request.
The eager_input_streaming field is optional. Setting it to true turns on fine-grained streaming for that tool, and omitting it gives you standard buffered streaming, in which the API buffers and validates each parameter value before streaming it back. The exception is a request that still sends the legacy fine-grained-tool-streaming-2025-05-14 beta header, which turns fine-grained streaming on for tools that leave the field unset. The per-tool field replaces that header, and an explicit false keeps buffered streaming for a tool even when a request still sends it. The legacy header cannot be combined with a computer use or browser use toolset entry: the API rejects a request that sends both, so remove the header and set eager_input_streaming on the user-defined tools that need it. See Tool reference for the field definition.
The following example turns on fine-grained streaming for a make_file tool and asks Haijun for a long poem, so the tool input is large enough to watch it stream in:
curl https://haijun.my.id/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $JUGLOW_API_KEY" \
-H "juglow-version: 2023-06-01" \
-d '{
"model": "haijun-opus-5-5",
"max_tokens": 65536,
"tools": [
{
"name": "make_file",
"description": "Write text to a file",
"eager_input_streaming": true,
"input_schema": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "The filename to write text to"
},
"lines_of_text": {
"type": "array",
"description": "An array of lines of text to write to the file"
}
},
"required": ["filename", "lines_of_text"]
}
}
],
"messages": [
{
"role": "user",
"content": "Can you write a long poem and make a file called poem.txt?"
}
],
"stream": true
}' ant messages create --stream --format jsonl <<'YAML' |
model: haijun-opus-5-5
max_tokens: 65536
tools:
- name: make_file
description: Write text to a file
eager_input_streaming: true
input_schema:
type: object
properties:
filename:
type: string
description: The filename to write text to
lines_of_text:
type: array
description: An array of lines of text to write to the file
required:
- filename
- lines_of_text
messages:
- role: user
content: Can you write a long poem and make a file called poem.txt?
YAML
jq -rj 'select(.delta.type == "input_json_delta") | .delta.partial_json' client = juglow.Juglow()
with client.messages.stream(
max_tokens=65536,
model="haijun-opus-5-5",
tools=[
{
"name": "make_file",
"description": "Write text to a file",
"eager_input_streaming": True,
"input_schema": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "The filename to write text to",
},
"lines_of_text": {
"type": "array",
"description": "An array of lines of text to write to the file",
},
},
"required": ["filename", "lines_of_text"],
},
}
],
messages=[
{
"role": "user",
"content": "Can you write a long poem and make a file called poem.txt?",
}
],
) as stream:
for event in stream:
if event.type == "input_json":
print(event.partial_json, end="", flush=True)
final_message = stream.get_final_message()
print()
for block in final_message.content:
if block.type == "tool_use":
print(f"Complete tool input: {block.input}") const client = new Juglow();
const stream = client.messages.stream({
model: "haijun-opus-5-5",
max_tokens: 65536,
tools: [
{
name: "make_file",
description: "Write text to a file",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {
filename: {
type: "string",
description: "The filename to write text to"
},
lines_of_text: {
type: "array",
description: "An array of lines of text to write to the file"
}
},
required: ["filename", "lines_of_text"]
}
}
],
messages: [
{
role: "user",
content: "Can you write a long poem and make a file called poem.txt?"
}
]
});
stream.on("inputJson", (partialJson) => {
process.stdout.write(partialJson);
});
const message = await stream.finalMessage();
console.log();
for (const block of message.content) {
if (block.type === "tool_use") {
console.log("Complete tool input:", block.input);
}
} JuglowClient client = new();
MessageCreateParams parameters = new()
{
Model = Model.HaijunOpus5_5,
MaxTokens = 65536,
Tools =
[
new Tool
{
Name = "make_file",
Description = "Write text to a file",
EagerInputStreaming = true,
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["filename"] = JsonSerializer.SerializeToElement(
new { type = "string", description = "The filename to write text to" }
),
["lines_of_text"] = JsonSerializer.SerializeToElement(
new { type = "array", description = "An array of lines of text to write to the file" }
),
},
Required = ["filename", "lines_of_text"],
},
},
],
Messages =
[
new()
{
Role = Role.User,
Content = "Can you write a long poem and make a file called poem.txt?",
},
],
};
// The C# example assembles the input itself: content block index -> accumulated JSON
var toolInputs = new Dictionary<long, StringBuilder>();
await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
{
if (
streamEvent.TryPickContentBlockStart(out var start)
&& start.ContentBlock.TryPickToolUse(out _)
)
{
toolInputs[start.Index] = new StringBuilder();
}
else if (
streamEvent.TryPickContentBlockDelta(out var delta)
&& delta.Delta.TryPickInputJson(out var inputJson)
)
{
Console.Write(inputJson.PartialJson);
toolInputs[delta.Index].Append(inputJson.PartialJson);
}
}
Console.WriteLine();
foreach (var accumulatedInput in toolInputs.Values)
{
Console.WriteLine($"Complete tool input: {accumulatedInput}");
} client := juglow.NewClient()
makeFileTool := juglow.ToolParam{
Name: "make_file",
Description: juglow.String("Write text to a file"),
EagerInputStreaming: juglow.Bool(true),
InputSchema: juglow.ToolInputSchemaParam{
Properties: map[string]any{
"filename": map[string]any{
"type": "string",
"description": "The filename to write text to",
},
"lines_of_text": map[string]any{
"type": "array",
"description": "An array of lines of text to write to the file",
},
},
Required: []string{"filename", "lines_of_text"},
},
}
stream := client.Messages.NewStreaming(context.Background(), juglow.MessageNewParams{
Model: juglow.ModelHaijunOpus5_5,
MaxTokens: 65536,
Tools: []juglow.ToolUnionParam{{OfTool: &makeFileTool}},
Messages: []juglow.MessageParam{
juglow.NewUserMessage(juglow.NewTextBlock(
"Can you write a long poem and make a file called poem.txt?",
)),
},
})
message := juglow.Message{}
for stream.Next() {
event := stream.Current()
if err := message.Accumulate(event); err != nil {
panic(err)
}
if delta, ok := event.AsAny().(juglow.ContentBlockDeltaEvent); ok {
if inputJSON, ok := delta.Delta.AsAny().(juglow.InputJSONDelta); ok {
fmt.Print(inputJSON.PartialJSON)
}
}
}
if err := stream.Err(); err != nil {
panic(err)
}
fmt.Println()
for _, block := range message.Content {
if toolUse, ok := block.AsAny().(juglow.ToolUseBlock); ok {
fmt.Printf("Complete tool input: %s\n", toolUse.Input)
}
} JuglowClient client = JuglowOkHttpClient.fromEnv();
Tool makeFileTool = Tool.builder()
.name("make_file")
.description("Write text to a file")
.eagerInputStreaming(true)
.inputSchema(Tool.InputSchema.builder()
.properties(Tool.InputSchema.Properties.builder()
.putAdditionalProperty("filename", JsonValue.from(Map.of(
"type", "string",
"description", "The filename to write text to")))
.putAdditionalProperty("lines_of_text", JsonValue.from(Map.of(
"type", "array",
"description", "An array of lines of text to write to the file")))
.build())
.addRequired("filename")
.addRequired("lines_of_text")
.build())
.build();
MessageCreateParams params = MessageCreateParams.builder()
.model(Model.HAIJUN_OPUS_5_5)
.maxTokens(65536L)
.addTool(makeFileTool)
.addUserMessage("Can you write a long poem and make a file called poem.txt?")
.build();
MessageAccumulator accumulator = MessageAccumulator.create();
try (StreamResponse<RawMessageStreamEvent> streamResponse =
client.messages().createStreaming(params)) {
streamResponse.stream().forEach(event -> {
accumulator.accumulate(event);
if (event.isContentBlockDelta()) {
var delta = event.asContentBlockDelta().delta();
if (delta.isInputJson()) {
IO.print(delta.asInputJson().partialJson());
}
}
});
}
IO.println("");
accumulator.message().content().forEach(block ->
block.toolUse().ifPresent(toolUse ->
IO.println("Complete tool input: " + toolUse._input()))); use Juglow\Client;
use Juglow\Messages\InputJSONDelta;
use Juglow\Messages\Model;
use Juglow\Messages\RawContentBlockDeltaEvent;
use Juglow\Messages\RawContentBlockStartEvent;
use Juglow\Messages\ToolUseBlock;
$client = new Client();
$stream = $client->messages->createStream(
maxTokens: 65536,
model: Model::HAIJUN_OPUS_5_5,
tools: [
[
'name' => 'make_file',
'description' => 'Write text to a file',
'eager_input_streaming' => true,
'input_schema' => [
'type' => 'object',
'properties' => [
'filename' => [
'type' => 'string',
'description' => 'The filename to write text to',
],
'lines_of_text' => [
'type' => 'array',
'description' => 'An array of lines of text to write to the file',
],
],
'required' => ['filename', 'lines_of_text'],
],
],
],
messages: [
[
'role' => 'user',
'content' => 'Can you write a long poem and make a file called poem.txt?',
],
],
);
// The PHP example assembles the input itself: index => accumulated JSON string
$toolInputs = [];
foreach ($stream as $event) {
switch (true) {
case $event instanceof RawContentBlockStartEvent:
if ($event->contentBlock instanceof ToolUseBlock) {
$toolInputs[$event->index] = '';
}
break;
case $event instanceof RawContentBlockDeltaEvent:
if ($event->delta instanceof InputJSONDelta) {
echo $event->delta->partialJSON;
$toolInputs[$event->index] .= $event->delta->partialJSON;
}
break;
}
}
echo "\n";
foreach ($toolInputs as $toolInput) {
echo "Complete tool input: {$toolInput}\n";
} client = Juglow::Client.new
stream = client.messages.stream(
model: Juglow::Models::Model::HAIJUN_OPUS_5_5,
max_tokens: 65_536,
tools: [
{
name: "make_file",
description: "Write text to a file",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {
filename: {
type: "string",
description: "The filename to write text to"
},
lines_of_text: {
type: "array",
description: "An array of lines of text to write to the file"
}
},
required: ["filename", "lines_of_text"]
}
}
],
messages: [
{
role: "user",
content: "Can you write a long poem and make a file called poem.txt?"
}
]
)
stream.each do |event|
print event.partial_json if event.is_a?(Juglow::Streaming::InputJsonEvent)
end
puts
stream.accumulated_message.content.each do |block|
puts "Complete tool input: #{block.input}" if block.type == :tool_use
endEvery tab turns on fine-grained streaming for the make_file tool. The SDK tabs print each input fragment the moment it arrives, then print the complete accumulated input once the stream ends. The cURL tab shows the raw event stream, and the CLI tab uses jq to print just the fragments. Because the printed fragments join into the full tool input, the poem fills your terminal as Haijun writes it:
{"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", "", "I.", "", "Beneath the vast and star-strewn sky,", "Where silver moonbeams softly lie,", ...
Complete tool input: {"filename": "poem.txt", "lines_of_text": ["The Wanderer's Journey", ...]}Without eager_input_streaming, the API buffers and validates each parameter value before streaming it back, so nothing prints for a large parameter until Haijun has finished generating it. With it, fragments start arriving as soon as Haijun begins the parameter, and they are typically longer, with fewer mid-word breaks.
Accumulating tool input deltas
The accumulation contract is the same as for standard tool-use streaming, so this section applies with and without eager_input_streaming. See Input JSON delta in Streaming messages for the event format. Fine-grained tool streaming changes what you can assume about the result: the server streams fragments without validating them, so the accumulated string might not be valid JSON.
When a tool_use content block streams, the initial content_block_start event contains input: {} (an empty object). This is a placeholder. The actual input arrives as a series of input_json_delta events, each carrying a partial_json string fragment. To assemble the full input, concatenate these fragments and parse the result when the block closes.
Where your SDK provides an accumulator helper (as the Python, TypeScript, Go, Java, and Ruby tabs in the previous example do), it handles this for you. The manual pattern is for SDKs without a helper, or when you want full control over how the input is assembled.
The accumulation contract:
- On
content_block_startwithtype: "tool_use", initialize an empty string:input_json = ""
- For each
content_block_deltawithtype: "input_json_delta", append:input_json += event.delta.partial_json
- On
content_block_stop, parse the accumulated string
Guard the parse, as the following SDK examples do. A response can also stop at max_tokens midway through a parameter. Check the stop reason and decide whether to retry the request with a higher max_tokens or repair the partial input.
The type mismatch between the initial input: {} (object) and partial_json (string) is by design. The empty object marks the slot in the content array. The delta strings build the real value.
# Accumulating per-block input deltas needs a programming language; the first
# example's CLI tab shows the raw fragments with jq. See the SDK tabs. # Accumulating per-block input deltas needs a programming language; the first
# example's CLI tab shows the raw fragments with jq. See the SDK tabs. client = juglow.Juglow()
tool_inputs: dict[int, str] = {} # index -> accumulated JSON string
with client.messages.stream(
model="haijun-opus-5-5",
max_tokens=1024,
tools=[
{
"name": "get_weather",
"description": "Get current weather for a city",
"eager_input_streaming": True,
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
],
messages=[{"role": "user", "content": "Weather in Paris?"}],
) as stream:
for event in stream:
match event.type:
case "content_block_start" if event.content_block.type == "tool_use":
tool_inputs[event.index] = ""
case "content_block_delta" if event.delta.type == "input_json_delta":
tool_inputs[event.index] += event.delta.partial_json
case "content_block_stop" if event.index in tool_inputs:
raw_input = tool_inputs[event.index]
try:
parsed = json.loads(raw_input)
except json.JSONDecodeError:
# The accumulated string is not guaranteed to be valid JSON.
# See "Handling invalid JSON in tool responses" on this page.
print(f"Invalid tool input: {raw_input}")
else:
print(f"Tool input: {parsed}") const client = new Juglow();
const toolInputs = new Map<number, string>();
const stream = client.messages.stream({
model: "haijun-opus-5-5",
max_tokens: 1024,
tools: [
{
name: "get_weather",
description: "Get current weather for a city",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}
],
messages: [{ role: "user", content: "Weather in Paris?" }]
});
for await (const event of stream) {
switch (event.type) {
case "content_block_start":
if (event.content_block.type === "tool_use") {
toolInputs.set(event.index, "");
}
break;
case "content_block_delta":
if (event.delta.type === "input_json_delta") {
toolInputs.set(
event.index,
(toolInputs.get(event.index) ?? "") + event.delta.partial_json
);
}
break;
case "content_block_stop":
if (toolInputs.has(event.index)) {
const rawInput = toolInputs.get(event.index)!;
try {
console.log("Tool input:", JSON.parse(rawInput));
} catch {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
console.log("Invalid tool input:", rawInput);
}
}
break;
}
} JuglowClient client = new();
MessageCreateParams parameters = new()
{
Model = Model.HaijunOpus5_5,
MaxTokens = 1024,
Tools =
[
new Tool
{
Name = "get_weather",
Description = "Get current weather for a city",
EagerInputStreaming = true,
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["city"] = JsonSerializer.SerializeToElement(new { type = "string" }),
},
Required = ["city"],
},
},
],
Messages = [new() { Role = Role.User, Content = "Weather in Paris?" }],
};
// Block index -> accumulated JSON fragments
// This example accumulates the deltas manually to show the raw stream;
// the SDK's MessageContentAggregator can also accumulate tool input automatically.
var toolInputs = new Dictionary<long, StringBuilder>();
await foreach (var streamEvent in client.Messages.CreateStreaming(parameters))
{
if (
streamEvent.TryPickContentBlockStart(out var start)
&& start.ContentBlock.TryPickToolUse(out _)
)
{
toolInputs[start.Index] = new StringBuilder();
}
else if (
streamEvent.TryPickContentBlockDelta(out var delta)
&& delta.Delta.TryPickInputJson(out var inputJson)
)
{
toolInputs[delta.Index].Append(inputJson.PartialJson);
}
else if (
streamEvent.TryPickContentBlockStop(out var stop)
&& toolInputs.TryGetValue(stop.Index, out var accumulated)
)
{
try
{
using var parsed = JsonDocument.Parse(accumulated.ToString());
Console.WriteLine($"Tool input: {parsed.RootElement}");
}
catch (JsonException)
{
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
Console.WriteLine($"Invalid tool input: {accumulated}");
}
}
} client := juglow.NewClient()
toolInputs := map[int64]string{} // content block index -> accumulated JSON
stream := client.Messages.NewStreaming(context.Background(), juglow.MessageNewParams{
Model: juglow.ModelHaijunOpus5_5,
MaxTokens: 1024,
Tools: []juglow.ToolUnionParam{{
OfTool: &juglow.ToolParam{
Name: "get_weather",
Description: juglow.String("Get current weather for a city"),
EagerInputStreaming: juglow.Bool(true),
InputSchema: juglow.ToolInputSchemaParam{
Properties: map[string]any{
"city": map[string]any{"type": "string"},
},
Required: []string{"city"},
},
},
}},
Messages: []juglow.MessageParam{
juglow.NewUserMessage(juglow.NewTextBlock("Weather in Paris?")),
},
})
for stream.Next() {
switch event := stream.Current().AsAny().(type) {
case juglow.ContentBlockStartEvent:
if _, ok := event.ContentBlock.AsAny().(juglow.ToolUseBlock); ok {
toolInputs[event.Index] = ""
}
case juglow.ContentBlockDeltaEvent:
if delta, ok := event.Delta.AsAny().(juglow.InputJSONDelta); ok {
toolInputs[event.Index] += delta.PartialJSON
}
case juglow.ContentBlockStopEvent:
if accumulated, ok := toolInputs[event.Index]; ok {
var parsed map[string]any
if err := json.Unmarshal([]byte(accumulated), &parsed); err != nil {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
fmt.Println("Invalid tool input:", accumulated)
} else {
fmt.Println("Tool input:", parsed)
}
}
}
}
if err := stream.Err(); err != nil {
panic(err)
} JuglowClient client = JuglowOkHttpClient.fromEnv();
ObjectMapper objectMapper = new ObjectMapper();
Tool weatherTool = Tool.builder()
.name("get_weather")
.description("Get current weather for a city")
.eagerInputStreaming(true)
.inputSchema(Tool.InputSchema.builder()
.properties(Tool.InputSchema.Properties.builder()
.putAdditionalProperty("city", JsonValue.from(Map.of("type", "string")))
.build())
.addRequired("city")
.build())
.build();
MessageCreateParams createParams = MessageCreateParams.builder()
.model(Model.HAIJUN_OPUS_5_5)
.maxTokens(1024)
.addTool(weatherTool)
.addUserMessage("Weather in Paris?")
.build();
// Content block index -> accumulated tool input JSON
Map<Long, StringBuilder> toolInputs = new HashMap<>();
try (StreamResponse<RawMessageStreamEvent> streamResponse = client.messages().createStreaming(createParams)) {
var eventIterator = streamResponse.stream().iterator();
while (eventIterator.hasNext()) {
RawMessageStreamEvent event = eventIterator.next();
switch (event.type().value()) {
case CONTENT_BLOCK_START -> {
var blockStart = event.asContentBlockStart();
if (blockStart.contentBlock().isToolUse()) {
toolInputs.put(blockStart.index(), new StringBuilder());
}
}
case CONTENT_BLOCK_DELTA -> {
var blockDelta = event.asContentBlockDelta();
if (blockDelta.delta().isInputJson() && toolInputs.containsKey(blockDelta.index())) {
toolInputs.get(blockDelta.index()).append(blockDelta.delta().asInputJson().partialJson());
}
}
case CONTENT_BLOCK_STOP -> {
var blockStop = event.asContentBlockStop();
if (toolInputs.containsKey(blockStop.index())) {
String accumulated = toolInputs.get(blockStop.index()).toString();
try {
IO.println("Tool input: " + objectMapper.readTree(accumulated));
} catch (JsonProcessingException e) {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
IO.println("Invalid tool input: " + accumulated);
}
}
}
}
}
} use Juglow\Client;
use Juglow\Messages\InputJSONDelta;
use Juglow\Messages\Model;
use Juglow\Messages\RawContentBlockDeltaEvent;
use Juglow\Messages\RawContentBlockStartEvent;
use Juglow\Messages\RawContentBlockStopEvent;
use Juglow\Messages\ToolUseBlock;
$client = new Client();
// The PHP SDK does not provide a stream accumulator for tool input;
// the manual pattern shown here is the supported approach.
$toolInputs = []; // index => accumulated JSON string
$stream = $client->messages->createStream(
maxTokens: 1024,
model: Model::HAIJUN_OPUS_5_5,
tools: [
[
'name' => 'get_weather',
'description' => 'Get current weather for a city',
'eager_input_streaming' => true,
'input_schema' => [
'type' => 'object',
'properties' => ['city' => ['type' => 'string']],
'required' => ['city'],
],
],
],
messages: [['role' => 'user', 'content' => 'Weather in Paris?']],
);
foreach ($stream as $event) {
switch (true) {
case $event instanceof RawContentBlockStartEvent:
if ($event->contentBlock instanceof ToolUseBlock) {
$toolInputs[$event->index] = '';
}
break;
case $event instanceof RawContentBlockDeltaEvent:
if ($event->delta instanceof InputJSONDelta) {
$toolInputs[$event->index] .= $event->delta->partialJSON;
}
break;
case $event instanceof RawContentBlockStopEvent:
if (isset($toolInputs[$event->index])) {
$accumulated = $toolInputs[$event->index];
try {
$parsed = json_decode($accumulated, associative: true, flags: JSON_THROW_ON_ERROR);
echo "Tool input: " . json_encode($parsed) . "\n";
} catch (JsonException $e) {
// The accumulated string is not guaranteed to be valid JSON.
// See "Handling invalid JSON in tool responses" on this page.
echo "Invalid tool input: {$accumulated}\n";
}
}
break;
}
} client = Juglow::Client.new
tool_inputs = {} # index -> accumulated JSON string
stream = client.messages.stream_raw(
model: Juglow::Models::Model::HAIJUN_OPUS_5_5,
max_tokens: 1024,
tools: [
{
name: "get_weather",
description: "Get current weather for a city",
eager_input_streaming: true,
input_schema: {
type: "object",
properties: {city: {type: "string"}},
required: ["city"]
}
}
],
messages: [{role: "user", content: "Weather in Paris?"}]
)
stream.each do |event|
case event
when Juglow::Models::RawContentBlockStartEvent
tool_inputs[event.index] = +"" if event.content_block.type == :tool_use
when Juglow::Models::RawContentBlockDeltaEvent
if event.delta.is_a?(Juglow::Models::InputJSONDelta)
tool_inputs[event.index] << event.delta.partial_json
end
when Juglow::Models::RawContentBlockStopEvent
if tool_inputs.key?(event.index)
accumulated = tool_inputs[event.index]
begin
parsed = JSON.parse(accumulated)
puts "Tool input: #{parsed}"
rescue JSON::ParserError
# The accumulated string is not guaranteed to be valid JSON.
# See "Handling invalid JSON in tool responses" on this page.
puts "Invalid tool input: #{accumulated}"
end
end
end
endTip: Reacting to fragments and assembling them are separate concerns. The first example reacts to each fragment as it arrives and still hands assembly to the SDK in the tabs that use an accumulator helper. Use the manual pattern when you are not using an accumulator helper or when you want full control over assembly.
Handling invalid JSON in tool responses
With fine-grained tool streaming, the accumulated input for a tool call might be invalid or incomplete JSON. When it is, you cannot run the tool, so report the failure back to Haijun instead. The content of a tool result does not have to be JSON, but wrapping the raw string in a JSON object under a single key makes it unambiguous to Haijun that you received invalid JSON, and preserves the original input for debugging:
{
"INVALID_JSON": "<the unparseable input you received>"
}Return the wrapper, serialized to a string, as the content of a tool result content block with is_error set to true:
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"is_error": true,
"content": "{\"INVALID_JSON\": \"<the unparseable input you received>\"}"
}Note: Build the wrapper with your JSON library rather than by concatenating strings, so quotes and other special characters in the invalid input are escaped correctly.
Next steps
Understand how the context window works, how extended thinking and tool use count toward it, and how to manage context as conversations grow.
Stream Messages API responses incrementally with server-sent events, including text, tool use, and extended thinking deltas.
Parse tool\_use blocks, format tool\_result responses, and handle errors with is\_error.
Directory of Juglow-provided tools and reference for optional tool definition properties.