@@ -21,21 +21,191 @@ function resolveContextToolNames(context: Parameters<StreamFn>[1]): Set<string>
2121 return new Set ( names ) ;
2222}
2323
24- function couldStillBePlainTextToolCall ( text : string ) : boolean {
24+ function couldStillBePlainTextToolCall ( text : string , toolNames : Set < string > ) : boolean {
2525 if ( text . length > 256_000 ) {
2626 return false ;
2727 }
2828 const trimmed = text . trimStart ( ) ;
2929 return (
3030 trimmed . length === 0 ||
31- trimmed . startsWith ( "[" ) ||
32- trimmed . startsWith ( "<|channel|>" ) ||
33- trimmed . startsWith ( "commentary" ) ||
34- trimmed . startsWith ( "analysis" ) ||
35- trimmed . startsWith ( "final" )
31+ couldStillBeBracketedToolCall ( trimmed , toolNames ) ||
32+ couldStillBeHarmonyToolCall ( trimmed , toolNames )
3633 ) ;
3734}
3835
36+ function matchesLiteralPrefix ( text : string , literal : string ) : boolean {
37+ return literal . startsWith ( text ) || text . startsWith ( literal ) ;
38+ }
39+
40+ function skipHorizontalWhitespace ( text : string , start : number ) : number {
41+ let cursor = start ;
42+ while ( text [ cursor ] === " " || text [ cursor ] === "\t" ) {
43+ cursor += 1 ;
44+ }
45+ return cursor ;
46+ }
47+
48+ function isToolNameChar ( char : string | undefined ) : boolean {
49+ return Boolean ( char && / [ A - Z a - z 0 - 9 _ - ] / . test ( char ) ) ;
50+ }
51+
52+ function hasToolNamePrefix ( toolNames : Set < string > , prefix : string ) : boolean {
53+ for ( const toolName of toolNames ) {
54+ if ( toolName . startsWith ( prefix ) ) {
55+ return true ;
56+ }
57+ }
58+ return false ;
59+ }
60+
61+ function couldStillBeJsonPayload ( text : string , start : number ) : boolean {
62+ let cursor = start ;
63+ while ( cursor < text . length && / \s / . test ( text [ cursor ] ?? "" ) ) {
64+ cursor += 1 ;
65+ }
66+ return cursor >= text . length || text [ cursor ] === "{" ;
67+ }
68+
69+ function couldStillBeBracketedToolCall ( text : string , toolNames : Set < string > ) : boolean {
70+ if ( ! text . startsWith ( "[" ) ) {
71+ return false ;
72+ }
73+
74+ const toolPrefix = "[tool:" ;
75+ if ( matchesLiteralPrefix ( text , toolPrefix ) ) {
76+ if ( text . length <= toolPrefix . length ) {
77+ return true ;
78+ }
79+ let cursor = toolPrefix . length ;
80+ while ( isToolNameChar ( text [ cursor ] ) ) {
81+ cursor += 1 ;
82+ }
83+ const name = text . slice ( toolPrefix . length , cursor ) ;
84+ if ( ! name || ! hasToolNamePrefix ( toolNames , name ) ) {
85+ return false ;
86+ }
87+ if ( cursor >= text . length ) {
88+ return true ;
89+ }
90+ if ( text [ cursor ] !== "]" ) {
91+ return false ;
92+ }
93+ return couldStillBeJsonPayload ( text , cursor + 1 ) ;
94+ }
95+
96+ let cursor = 1 ;
97+ while ( isToolNameChar ( text [ cursor ] ) ) {
98+ cursor += 1 ;
99+ }
100+ const name = text . slice ( 1 , cursor ) ;
101+ if ( ! name || ! hasToolNamePrefix ( toolNames , name ) ) {
102+ return false ;
103+ }
104+ if ( cursor >= text . length ) {
105+ return true ;
106+ }
107+ if ( text [ cursor ] !== "]" ) {
108+ return false ;
109+ }
110+
111+ cursor = skipHorizontalWhitespace ( text , cursor + 1 ) ;
112+ if ( cursor >= text . length ) {
113+ return true ;
114+ }
115+ if ( text [ cursor ] === "\r" ) {
116+ if ( cursor + 1 >= text . length ) {
117+ return true ;
118+ }
119+ return couldStillBeJsonPayload ( text , text [ cursor + 1 ] === "\n" ? cursor + 2 : cursor + 1 ) ;
120+ }
121+ if ( text [ cursor ] !== "\n" ) {
122+ return false ;
123+ }
124+ return couldStillBeJsonPayload ( text , cursor + 1 ) ;
125+ }
126+
127+ function couldStillBeHarmonyToolCall ( text : string , toolNames : Set < string > ) : boolean {
128+ const channelMarker = "<|channel|>" ;
129+ let cursor = 0 ;
130+ if ( matchesLiteralPrefix ( text , channelMarker ) ) {
131+ if ( text . length <= channelMarker . length ) {
132+ return true ;
133+ }
134+ cursor = channelMarker . length ;
135+ }
136+
137+ const rest = text . slice ( cursor ) ;
138+ const channel = [ "commentary" , "analysis" , "final" ] . find ( ( candidate ) =>
139+ matchesLiteralPrefix ( rest , candidate ) ,
140+ ) ;
141+ if ( ! channel ) {
142+ return false ;
143+ }
144+ if ( rest . length <= channel . length ) {
145+ return true ;
146+ }
147+
148+ cursor += channel . length ;
149+ cursor = skipHorizontalWhitespace ( text , cursor ) ;
150+ if ( cursor >= text . length ) {
151+ return true ;
152+ }
153+
154+ const toMarker = "to=" ;
155+ const toRest = text . slice ( cursor ) ;
156+ if ( ! matchesLiteralPrefix ( toRest , toMarker ) ) {
157+ return false ;
158+ }
159+ if ( toRest . length <= toMarker . length ) {
160+ return true ;
161+ }
162+
163+ cursor += toMarker . length ;
164+ const nameStart = cursor ;
165+ while ( isToolNameChar ( text [ cursor ] ) ) {
166+ cursor += 1 ;
167+ }
168+ const name = text . slice ( nameStart , cursor ) ;
169+ if ( ! name || ! hasToolNamePrefix ( toolNames , name ) ) {
170+ return false ;
171+ }
172+ if ( cursor >= text . length ) {
173+ return true ;
174+ }
175+
176+ cursor = skipHorizontalWhitespace ( text , cursor ) ;
177+ if ( cursor >= text . length ) {
178+ return true ;
179+ }
180+ if ( ! toolNames . has ( name ) ) {
181+ return false ;
182+ }
183+
184+ const codeMarker = "code" ;
185+ const codeRest = text . slice ( cursor ) ;
186+ if ( ! matchesLiteralPrefix ( codeRest , codeMarker ) ) {
187+ return false ;
188+ }
189+ if ( codeRest . length <= codeMarker . length ) {
190+ return true ;
191+ }
192+
193+ cursor += codeMarker . length ;
194+ while ( cursor < text . length && / \s / . test ( text [ cursor ] ?? "" ) ) {
195+ cursor += 1 ;
196+ }
197+ if ( cursor >= text . length ) {
198+ return true ;
199+ }
200+
201+ const messageMarker = "<|message|>" ;
202+ const messageRest = text . slice ( cursor ) ;
203+ if ( matchesLiteralPrefix ( messageRest , messageMarker ) ) {
204+ return true ;
205+ }
206+ return text [ cursor ] === "{" ;
207+ }
208+
39209function createSyntheticToolCallId ( ) : string {
40210 return `call_${ randomUUID ( ) . replace ( / - / g, "" ) . slice ( 0 , 24 ) } ` ;
41211}
@@ -179,7 +349,7 @@ function wrapPlainTextToolCallStream(
179349 } else if ( typeof record ?. content === "string" && ! bufferedText ) {
180350 bufferedText = record . content ;
181351 }
182- if ( ! couldStillBePlainTextToolCall ( bufferedText ) ) {
352+ if ( ! couldStillBePlainTextToolCall ( bufferedText , toolNames ) ) {
183353 flushBufferedTextEvents ( ) ;
184354 }
185355 continue ;
0 commit comments