Compare commits

..
2 Commits
Author SHA1 Message Date
Luxferre 9ee04ddf32 tool call vis improvement 2026-09-11 15:30:32 +03:00
Luxferre 69571704a1 tool call vis improvement 2026-09-11 15:16:40 +03:00
+81 -10
View File
@@ -834,6 +834,7 @@ type Message struct {
ReasoningContent string `json:"reasoning_content,omitempty"` ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"`
ToolCallsShown bool `json:"-"` // tool call headers already printed live during streaming
} }
type ToolCall struct { type ToolCall struct {
@@ -1133,6 +1134,53 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) {
var lastUsage Usage var lastUsage Usage
tcs := map[int]*ToolCall{} tcs := map[int]*ToolCall{}
var order []int var order []int
var toolCallsShown bool
// Progressive, per-token display of tool calls as they stream in, so the
// user watches the invocation being shaped token by token (like reasoning
// text) instead of a blank pause. Only used when stdout is a terminal;
// piped/redirected output is left untouched. Each fragment is appended
// directly to the terminal; the closing ")" is emitted only when the call is
// finalized, so the visible text is always a true prefix of the final form.
tty := isTerminal(int(os.Stdout.Fd()))
liveIdx := -1
livePrinted := ""
liveTarget := func(t *ToolCall, closed bool) string {
s := "[tool call: " + t.Function.Name
if closed || t.Function.Arguments != "" {
s += "(" + filterText(t.Function.Arguments)
if closed { s += ")]" }
}
return s
}
finishLive := func(closed bool) {
if liveIdx < 0 { return }
target := liveTarget(tcs[liveIdx], closed)
if len(target) > len(livePrinted) {
fmt.Print(c(target[len(livePrinted):], 33))
}
if closed { fmt.Print("\n") }
livePrinted = ""
liveIdx = -1
}
stepLive := func() {
if !tty || len(order) == 0 { return }
cur := order[len(order)-1]
if liveIdx != -1 && liveIdx != cur {
// A different tool call is now being shaped: finalize the previous one.
finishLive(true)
}
if liveIdx == -1 {
liveIdx = cur
livePrinted = ""
}
target := liveTarget(tcs[cur], false)
if len(target) > len(livePrinted) {
fmt.Print(c(target[len(livePrinted):], 33))
livePrinted = target
}
toolCallsShown = true
}
flushTable := func() { flushTable := func() {
if len(tblBuf) > 0 { if len(tblBuf) > 0 {
@@ -1199,16 +1247,32 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) {
} }
} }
} }
for _, tc := range dl.ToolCalls { if len(dl.ToolCalls) > 0 {
t, ok := tcs[tc.Index] if inReasoning {
if !ok { fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n")
t = &ToolCall{Type: "function"} inReasoning = false
tcs[tc.Index] = t
order = append(order, tc.Index)
} }
if tc.ID != "" { t.ID = tc.ID } if liveIdx == -1 {
if tc.Function.Name != "" { t.Function.Name += tc.Function.Name } // First tool call of the stream: flush any pending content line so it
if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments } // is not overwritten by the live tool-call line.
flushTable()
if lineBuf != "" {
fmt.Println(renderMDLine(lineBuf, &mdSt))
lineBuf = ""
}
}
for _, tc := range dl.ToolCalls {
t, ok := tcs[tc.Index]
if !ok {
t = &ToolCall{Type: "function"}
tcs[tc.Index] = t
order = append(order, tc.Index)
}
if tc.ID != "" { t.ID = tc.ID }
if tc.Function.Name != "" { t.Function.Name += tc.Function.Name }
if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments }
}
stepLive()
} }
} }
if err := ctx.Err(); err != nil { return Message{}, lastUsage, err } if err := ctx.Err(); err != nil { return Message{}, lastUsage, err }
@@ -1221,10 +1285,15 @@ func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) {
fmt.Println(renderMDLine(lineBuf, &mdSt)) fmt.Println(renderMDLine(lineBuf, &mdSt))
} }
} }
if liveIdx >= 0 {
// Finalize the in-progress tool-call line so following output starts fresh.
finishLive(true)
}
if inReasoning { if inReasoning {
fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n") fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n")
} }
m := Message{Role: "assistant"} m := Message{Role: "assistant"}
m.ToolCallsShown = toolCallsShown
if content != "" { m.Content = strp(content) } if content != "" { m.Content = strp(content) }
if reas != "" { m.ReasoningContent = reas } if reas != "" { m.ReasoningContent = reas }
if len(tcs) > 0 { if len(tcs) > 0 {
@@ -1393,7 +1462,9 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
for _, tc := range m.ToolCalls { for _, tc := range m.ToolCalls {
if err := ctx.Err(); err != nil { return msgs, turnUsage, err } if err := ctx.Err(); err != nil { return msgs, turnUsage, err }
fn, astr := tc.Function.Name, filterText(tc.Function.Arguments) fn, astr := tc.Function.Name, filterText(tc.Function.Arguments)
fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33)) if !m.ToolCallsShown {
fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33))
}
res, sty := "", 2 res, sty := "", 2
var a map[string]any var a map[string]any
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil { if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {