1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//! Tracks partial tool calls while streaming so `tool-input-start` and
//! `tool-input-delta` parts are emitted before the final `tool-call`.
use std::collections::HashSet;
use ferrin_spec::StreamPart;
use ferrin_spec::ToolCall;
use ferrin_spec::ToolCallId;
use ferrin_spec::ToolName;
/// Remembers which tool calls already emitted `tool-input-start`.
#[derive(Debug, Default, Clone)]
pub struct StreamingToolCallTracker {
started: HashSet<ToolCallId>,
}
impl StreamingToolCallTracker {
/// Creates an empty tracker.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Returns `true` if `id` already started.
#[must_use]
pub fn has_started(&self, id: &ToolCallId) -> bool {
self.started.contains(id)
}
/// Marks `id` as started, returning `true` the first time.
pub fn start(&mut self, id: ToolCallId) -> bool {
self.started.insert(id)
}
/// Emits the parts a tool call needs so consumers see a consistent
/// `tool-input-start` / (`tool-input-delta`) / `tool-input-end` /
/// `tool-call` sequence, even when the provider delivered the call whole.
pub fn parts_for_complete_call(
&mut self,
id: ToolCallId,
name: ToolName,
input: String,
provider_executed: bool,
) -> Vec<StreamPart> {
let mut parts = Vec::with_capacity(4);
if self.start(id.clone()) {
parts.push(StreamPart::ToolInputStart {
id: id.clone(),
tool_name: name.clone(),
provider_executed,
dynamic: false,
title: None,
provider_metadata: None,
});
if !input.is_empty() {
parts.push(StreamPart::ToolInputDelta {
id: id.clone(),
delta: input.clone(),
provider_metadata: None,
});
}
}
parts.push(StreamPart::ToolInputEnd {
id: id.clone(),
provider_metadata: None,
});
let mut call = ToolCall::new(id, name, input);
call.provider_executed = provider_executed;
parts.push(StreamPart::ToolCall(call));
parts
}
}