mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 00:45:29 +08:00
Add support for DSML wrapper aliases (<dsml|tool_calls>, <|tool_calls>, <|tool_calls>) alongside canonical XML. Normalize mixed DSML/canonical tags instead of rejecting them. Add tilde fence (~~~) support, fix nested fence and unclosed fence handling, support CDATA-protected fence content, and skip prose mentions when scanning for real tool blocks. Mirror all changes between Go and Node.js runtimes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
201 lines
4.3 KiB
Go
201 lines
4.3 KiB
Go
package toolstream
|
|
|
|
import (
|
|
"ds2api/internal/toolcall"
|
|
"strings"
|
|
)
|
|
|
|
type State struct {
|
|
pending strings.Builder
|
|
capture strings.Builder
|
|
capturing bool
|
|
codeFenceStack []int
|
|
codeFencePendingTicks int
|
|
codeFencePendingTildes int
|
|
codeFenceNotLineStart bool // inverted: zero-value false means "at line start"
|
|
pendingToolRaw string
|
|
pendingToolCalls []toolcall.ParsedToolCall
|
|
disableDeltas bool
|
|
toolNameSent bool
|
|
toolName string
|
|
toolArgsStart int
|
|
toolArgsSent int
|
|
toolArgsString bool
|
|
toolArgsDone bool
|
|
}
|
|
|
|
type Event struct {
|
|
Content string
|
|
ToolCalls []toolcall.ParsedToolCall
|
|
ToolCallDeltas []ToolCallDelta
|
|
}
|
|
|
|
type ToolCallDelta struct {
|
|
Index int
|
|
Name string
|
|
Arguments string
|
|
}
|
|
|
|
func (s *State) resetIncrementalToolState() {
|
|
s.disableDeltas = false
|
|
s.toolNameSent = false
|
|
s.toolName = ""
|
|
s.toolArgsStart = -1
|
|
s.toolArgsSent = -1
|
|
s.toolArgsString = false
|
|
s.toolArgsDone = false
|
|
}
|
|
|
|
func (s *State) noteText(content string) {
|
|
if !hasMeaningfulText(content) {
|
|
return
|
|
}
|
|
updateCodeFenceState(s, content)
|
|
}
|
|
|
|
func hasMeaningfulText(text string) bool {
|
|
return strings.TrimSpace(text) != ""
|
|
}
|
|
|
|
func insideCodeFenceWithState(state *State, text string) bool {
|
|
if state == nil {
|
|
return insideCodeFence(text)
|
|
}
|
|
simulated := simulateCodeFenceState(
|
|
state.codeFenceStack,
|
|
state.codeFencePendingTicks,
|
|
state.codeFencePendingTildes,
|
|
!state.codeFenceNotLineStart,
|
|
text,
|
|
)
|
|
return len(simulated.stack) > 0
|
|
}
|
|
|
|
func insideCodeFence(text string) bool {
|
|
if text == "" {
|
|
return false
|
|
}
|
|
return len(simulateCodeFenceState(nil, 0, 0, true, text).stack) > 0
|
|
}
|
|
|
|
func updateCodeFenceState(state *State, text string) {
|
|
if state == nil || !hasMeaningfulText(text) {
|
|
return
|
|
}
|
|
next := simulateCodeFenceState(
|
|
state.codeFenceStack,
|
|
state.codeFencePendingTicks,
|
|
state.codeFencePendingTildes,
|
|
!state.codeFenceNotLineStart,
|
|
text,
|
|
)
|
|
state.codeFenceStack = next.stack
|
|
state.codeFencePendingTicks = next.pendingTicks
|
|
state.codeFencePendingTildes = next.pendingTildes
|
|
state.codeFenceNotLineStart = !next.lineStart
|
|
}
|
|
|
|
type codeFenceSimulation struct {
|
|
stack []int
|
|
pendingTicks int
|
|
pendingTildes int
|
|
lineStart bool
|
|
}
|
|
|
|
func simulateCodeFenceState(stack []int, pendingTicks, pendingTildes int, lineStart bool, text string) codeFenceSimulation {
|
|
chunk := text
|
|
nextStack := append([]int(nil), stack...)
|
|
ticks := pendingTicks
|
|
tildes := pendingTildes
|
|
atLineStart := lineStart
|
|
|
|
flushPending := func() {
|
|
if ticks > 0 {
|
|
if atLineStart && ticks >= 3 {
|
|
applyFenceMarker(&nextStack, ticks) // positive = backtick
|
|
}
|
|
atLineStart = false
|
|
ticks = 0
|
|
}
|
|
if tildes > 0 {
|
|
if atLineStart && tildes >= 3 {
|
|
applyFenceMarker(&nextStack, -tildes) // negative = tilde
|
|
}
|
|
atLineStart = false
|
|
tildes = 0
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(chunk); i++ {
|
|
ch := chunk[i]
|
|
if ch == '`' {
|
|
if tildes > 0 {
|
|
// Mixed chars — flush tildes first.
|
|
flushPending()
|
|
}
|
|
ticks++
|
|
continue
|
|
}
|
|
if ch == '~' {
|
|
if ticks > 0 {
|
|
flushPending()
|
|
}
|
|
tildes++
|
|
continue
|
|
}
|
|
flushPending()
|
|
switch ch {
|
|
case '\n', '\r':
|
|
atLineStart = true
|
|
case ' ', '\t':
|
|
if atLineStart {
|
|
continue
|
|
}
|
|
atLineStart = false
|
|
default:
|
|
atLineStart = false
|
|
}
|
|
}
|
|
|
|
return codeFenceSimulation{
|
|
stack: nextStack,
|
|
pendingTicks: ticks,
|
|
pendingTildes: tildes,
|
|
lineStart: atLineStart,
|
|
}
|
|
}
|
|
|
|
// applyFenceMarker pushes or pops a fence marker on the stack.
|
|
// Positive values represent backtick fences, negative represent tilde fences.
|
|
// A closing marker must match the sign (type) of the opening marker.
|
|
func applyFenceMarker(stack *[]int, marker int) {
|
|
if stack == nil || marker == 0 {
|
|
return
|
|
}
|
|
if len(*stack) == 0 {
|
|
*stack = append(*stack, marker)
|
|
return
|
|
}
|
|
top := (*stack)[len(*stack)-1]
|
|
// Signs must match: backtick closes backtick, tilde closes tilde.
|
|
sameType := (top > 0 && marker > 0) || (top < 0 && marker < 0)
|
|
if !sameType {
|
|
// Different fence type — treat as nested.
|
|
*stack = append(*stack, marker)
|
|
return
|
|
}
|
|
absMarker := marker
|
|
absTop := top
|
|
if absMarker < 0 {
|
|
absMarker = -absMarker
|
|
}
|
|
if absTop < 0 {
|
|
absTop = -absTop
|
|
}
|
|
if absMarker >= absTop {
|
|
*stack = (*stack)[:len(*stack)-1]
|
|
return
|
|
}
|
|
*stack = append(*stack, marker)
|
|
}
|